From 2fc65820d4d1890930f6b09bafafbc070ba9f078 Mon Sep 17 00:00:00 2001 From: jycouet Date: Thu, 16 Nov 2023 22:39:21 +0100 Subject: [PATCH] :tada: NEW: starts to look good --- .../svelte-ux/src/lib/utils/number.test.ts | 167 +++++++++++------- packages/svelte-ux/src/lib/utils/number.ts | 77 +++++--- 2 files changed, 158 insertions(+), 86 deletions(-) diff --git a/packages/svelte-ux/src/lib/utils/number.test.ts b/packages/svelte-ux/src/lib/utils/number.test.ts index 460e2a578..345f84e22 100644 --- a/packages/svelte-ux/src/lib/utils/number.test.ts +++ b/packages/svelte-ux/src/lib/utils/number.test.ts @@ -79,12 +79,30 @@ describe('formatNumber()', () => { expect(actual).equal('1,234.568'); }); - it('formats number with currency USD', () => { + it('returns value with significant digits', () => { + const actual = formatNumber(1234.5678, { + // style: 'decimal', // Optional, default is decimal + notation: 'compact', + maximumSignificantDigits: 2, + }); + expect(actual).equal('1.2K'); + }); + + it('returns value with significant digits', () => { + const actual = formatNumber(1000, { + // style: 'decimal', // Optional, default is decimal + notation: 'compact', + minimumSignificantDigits: 2, + }); + expect(actual).equal('1.0K'); + }); + + it('formats number with currency USD by style', () => { const actual = formatNumber(1234.5678, { style: 'currency' }); expect(actual).equal('$1,234.57'); }); - it('formats number with currency USD', () => { + it('formats number with currency USD by currency', () => { const actual = formatNumber(1234.5678, { currency: 'USD' }); expect(actual).equal('$1,234.57'); }); @@ -94,78 +112,101 @@ describe('formatNumber()', () => { expect(actual).equal('£1,234.57'); }); - it('formats number with currency EUR', () => { + it('formats number with currency EUR only currency', () => { const actual = formatNumber(1234.5678, { currency: 'EUR' }); expect(actual).equal('€1,234.57'); }); - it('formats number with currency EUR', () => { + it('formats number with currency EUR with right local', () => { const actual = formatNumber(1234.5678, { locales: 'fr', currency: 'EUR' }); expect(actual).equal('1 234,57 €'); }); -}); - -describe('formatNumberAsStyle()', () => { - // it('returns empty string for null', () => { - // const actual = formatNumberAsStyle(null); - // expect(actual).equal(''); - // }); - - // it('returns empty string for undefined', () => { - // const actual = formatNumberAsStyle(undefined); - // expect(actual).equal(''); - // }); - - // it('returns value as string for style "none"', () => { - // const actual = formatNumberAsStyle(1234.5678, 'none'); - // expect(actual).equal('1234.5678'); - // }); - - // it('returns value with currency symbol for style "currency"', () => { - // const actual = formatNumberAsStyle(1234.5678, 'currency'); - // expect(actual).toString().startsWith('$'); - // }); it('returns value with percent symbol for style "percent"', () => { - const actual = formatNumberAsStyle(0.1234, 'percent'); - expect(actual).toString().endsWith('%'); + const actual = formatNumber(0.1234, { style: 'percent' }); + expect(actual).equal('12.34%'); }); it('returns value with percent symbol and no decimal for style "percentRound"', () => { - const actual = formatNumberAsStyle(0.1234, 'percentRound'); - expect(actual).toString().endsWith('%'); - expect(actual).not.toContain('.'); + const actual2 = formatNumber(0.1234, { style: 'percentRound' }); + expect(actual2).equal('12%'); + }); + + it('returns value with metric suffix for style "unit" & meters', () => { + const actual = formatNumber(1000, { + style: 'unit', + + unit: 'meter', + unitDisplay: 'narrow', + + notation: 'compact', + fractionDigits: 0, + }); + expect(actual).equal('1Km'); + }); + + it('byte 10B', () => { + const actual = formatNumber(10, { + style: 'unit', + unit: 'byte', + unitDisplay: 'narrow', + notation: 'compact', + fractionDigits: 0, + }); + expect(actual).equal('10B'); + }); + + it('byte 200KB', () => { + const actual = formatNumber(200000, { + style: 'unit', + unit: 'byte', + unitDisplay: 'narrow', + notation: 'compact', + fractionDigits: 0, + }); + expect(actual).equal('200KB'); + }); + + it('byte 50MB', () => { + const actual = formatNumber(50000000, { + style: 'unit', + unit: 'byte', + unitDisplay: 'narrow', + notation: 'compact', + fractionDigits: 0, + }); + expect(actual).equal('50MB'); + }); + + it('dollar 0', () => { + const actual = formatNumber(0, { + style: 'metric', + suffix: ' dollar', + }); + expect(actual).equal('0 dollar'); + }); + + it('dollars 10', () => { + const actual = formatNumber(10, { + style: 'metric', + suffix: ' dollar', + }); + expect(actual).equal('10 dollars'); + }); + + it('dollars 200K', () => { + const actual = formatNumber(200000, { + style: 'metric', + suffix: ' dollar', + }); + expect(actual).equal('200K dollars'); + }); + + it('dollars 50M', () => { + const actual = formatNumber(50000000, { + style: 'metric', + suffix: ' dollar', + }); + expect(actual).equal('50M dollars'); }); - - // it('returns value with no decimal for style "integer"', () => { - // const actual = formatNumberAsStyle(1234.5678, 'integer'); - // expect(actual).equal('1235'); - // }); - - it('returns value with metric suffix for style "metric"', () => { - const actual = formatNumberAsStyle(1000, 'metric'); - expect(actual).equal('1k'); - }); - - it('returns value with significant digits', () => { - const actual = formatNumberAsStyle(1234.5678, 'decimal', { significantDigits: 2 }); - expect(actual).equal('1.2k'); - }); - - it('returns value with precision for style "decimal"', () => { - const actual = formatNumberAsStyle(1234.5678); - expect(actual).equal('1,234.57'); - }); - - // it('returns value with currency symbol for style "currency" EUR fr', () => { - // const actual = formatNumberAsStyle(1234.5678, 'currency', { - // format: { - // decimal: ',', - // thousands: ' ', - // grouping: [3], - // currency: ['', ' €'], - // }, - // }); - // expect(actual).toBe('1 234,57 €'); - // }); }); diff --git a/packages/svelte-ux/src/lib/utils/number.ts b/packages/svelte-ux/src/lib/utils/number.ts index f7b4cfbc9..bf55069bc 100644 --- a/packages/svelte-ux/src/lib/utils/number.ts +++ b/packages/svelte-ux/src/lib/utils/number.ts @@ -1,12 +1,31 @@ import { format as d3Format, formatDefaultLocale, type FormatLocaleDefinition } from 'd3-format'; +export type FormatNumberStyle = + | 'decimal' // from Intl.NumberFormat options.style NumberFormatOptions + | 'currency' // from Intl.NumberFormat options.style NumberFormatOptions + | 'percent' // from Intl.NumberFormat options.style NumberFormatOptions + | 'unit' // from Intl.NumberFormat options.style NumberFormatOptions + | 'none' + | 'integer' + | 'percentRound' + | 'metric'; + +type FormatNumberOptions = Intl.NumberFormatOptions & { + style?: FormatNumberStyle; + locales?: string | undefined; + fractionDigits?: number; +}; + // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat export function formatNumber( number: number | null | undefined, - options: Intl.NumberFormatOptions & { - style?: FormatNumberStyle; - locales?: string | undefined; - fractionDigits?: number; + options: FormatNumberOptions & { + suffix?: string; + /** + * If number is >= 2, then this extraSuffix will be appended + * @default 's' + */ + suffixExtraIfMany?: string; } = {} ) { if (number == null) { @@ -21,41 +40,53 @@ export function formatNumber( return `${parseInt(number.toString())}`; } - const defaultCurrency = 'USD'; + // todo set defaults in a context or something + const defaults: FormatNumberOptions = { currency: 'USD', fractionDigits: 2 }; - const formatter = Intl.NumberFormat(options.locales ?? undefined, { - // Let's always set a default currency, even if it's not used - currency: defaultCurrency, + const formatter = Intl.NumberFormat(options.locales ?? defaults.locales ?? undefined, { + // Let's always starts with all defaults + ...defaults, // If currency is specified, then style must be currency ...(options.currency != null && { style: 'currency', }), - // Let's always default to 2 fraction digits by default + // Let's shorten min / max with fractionDigits ...{ - minimumFractionDigits: options.fractionDigits != null ? options.fractionDigits : 2, - maximumFractionDigits: options.fractionDigits != null ? options.fractionDigits : 2, + minimumFractionDigits: + options.fractionDigits != null ? options.fractionDigits : defaults.fractionDigits, + maximumFractionDigits: + options.fractionDigits != null ? options.fractionDigits : defaults.fractionDigits, }, // now we bring in user specified options ...options, + + // Let's overwrite for style=percentRound + ...(options.style === 'percentRound' && { + style: 'percent', + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }), + + // Let's overwrite for style=metric + ...(options.style === 'metric' && { + style: 'decimal', + notation: 'compact', + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }), }); const value = formatter.format(number); - return value; -} + let suffix = options.suffix ?? ''; + if (suffix && Math.abs(number) >= 2 && options.suffixExtraIfMany !== '') { + suffix += options.suffixExtraIfMany ?? 's'; + } -export type FormatNumberStyle = - | 'decimal' // from Intl.NumberFormat options.style NumberFormatOptions - | 'currency' // from Intl.NumberFormat options.style NumberFormatOptions - | 'percent' // from Intl.NumberFormat options.style NumberFormatOptions - | 'unit' // from Intl.NumberFormat options.style NumberFormatOptions - | 'integer' - | 'percentRound' - | 'metric' // todo remove? Use unit instead? - | 'none' - | undefined; + return `${value}${suffix}`; +} export function formatNumberAsStyle( value: number | null | undefined,