Skip to content

Commit

Permalink
🎉 NEW: starts to look good
Browse files Browse the repository at this point in the history
  • Loading branch information
jycouet committed Nov 16, 2023
1 parent 2109749 commit 2fc6582
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 86 deletions.
167 changes: 104 additions & 63 deletions packages/svelte-ux/src/lib/utils/number.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand All @@ -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 €');
// });
});
77 changes: 54 additions & 23 deletions packages/svelte-ux/src/lib/utils/number.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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,
Expand Down

0 comments on commit 2fc6582

Please sign in to comment.