formatNumber function
Formats a number like Angular's DecimalPipe: digits driven by digitsInfo, separators of a locale. NaN, infinities and -0 are handled like formatDecimal.
ts
import { formatNumber } from 'utils';Signature
ts
export function formatNumber(value: number, digitsInfo = '1.0-3', locale = 'en-US', useGrouping = false): stringParameters
| Name | Description |
|---|---|
value | The number to format. |
digitsInfo | '{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}', such as '1.0-2'. Defaults to '1.0-3'. |
locale | The BCP 47 locale, such as 'fr-FR'. Defaults to 'en-US'. |
useGrouping | Whether to separate the thousands. Defaults to false. |
Returns
The formatted number.
Errors
- Throws TypeError: When
digitsInfodoes not look like'1.0-3'.
Example
ts
formatNumber(1234.5678); // '1234.568'
formatNumber(5, '3.0-2'); // '005'
formatNumber(1234.5, '1.2-2', 'en-US', true); // '1,234.50'
formatNumber(1234.5, '1.2-2', 'de-DE', true); // '1.234,50'Tests
Line coverage: 100 %.
- ✅ formats 3.141592653589793 with 1.0-2 as 3.14
- ✅ formats 5 with 3.0-2 as 005
- ✅ formats 1234.5 with 1.2-2 as 1234.50
- ✅ formats 1234567.891 with 1.0-2 as 1234567.89
- ✅ formats 0.5 with 1.0-0 as 1
- ✅ formats -0.001 with 1.0-2 as 0
- ✅ formats 1.2 with 1.5-5 as 1.20000
- ✅ uses the separators and grouping of the locale when grouping
- ✅ takes 1.0-3, en-US and no grouping
- ✅ formats NaN and infinities like String()
- ✅ returns the same output from the cached formatter
- ✅ throws a TypeError for digitsInfo ""
- ✅ throws a TypeError for digitsInfo "1"
- ✅ throws a TypeError for digitsInfo "1-2"
- ✅ throws a TypeError for digitsInfo ".1-1"
- ✅ throws a TypeError for digitsInfo "2."
- ✅ throws a TypeError for digitsInfo "1.5"
- ✅ throws a TypeError for digitsInfo "a.0-2"
Source
src/format/format-number.ts (on GitHub)
Source code
ts
const DIGITS_INFO_PATTERN = /^(?<minInteger>\d+)\.(?<minFraction>\d+)-(?<maxFraction>\d+)$/v;
const formatters = [
new Map<string, Map<string, Intl.NumberFormat>>(),
new Map<string, Map<string, Intl.NumberFormat>>(),
] as const;
function createFormatter(digitsInfo: string, locale: string, useGrouping: boolean): Intl.NumberFormat {
const groups = DIGITS_INFO_PATTERN.exec(digitsInfo)?.groups;
if (!groups) {
throw new TypeError(`digitsInfo must look like '1.0-3', got '${digitsInfo}'`);
}
return new Intl.NumberFormat(locale, {
minimumIntegerDigits: Number(groups['minInteger']),
minimumFractionDigits: Number(groups['minFraction']),
maximumFractionDigits: Number(groups['maxFraction']),
useGrouping,
signDisplay: 'negative',
});
}
/**
* Formats a number like Angular's `DecimalPipe`: digits driven by `digitsInfo`, separators of a locale.
* `NaN`, infinities and `-0` are handled like `formatDecimal`.
*
* @param value - The number to format.
* @param digitsInfo - `'{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}'`, such as `'1.0-2'`.
* Defaults to `'1.0-3'`.
* @param locale - The BCP 47 locale, such as `'fr-FR'`. Defaults to `'en-US'`.
* @param useGrouping - Whether to separate the thousands. Defaults to `false`.
* @returns The formatted number.
* @throws {TypeError} When `digitsInfo` does not look like `'1.0-3'`.
* @example
* formatNumber(1234.5678); // '1234.568'
* formatNumber(5, '3.0-2'); // '005'
* formatNumber(1234.5, '1.2-2', 'en-US', true); // '1,234.50'
* formatNumber(1234.5, '1.2-2', 'de-DE', true); // '1.234,50'
*/
export function formatNumber(value: number, digitsInfo = '1.0-3', locale = 'en-US', useGrouping = false): string {
if (!Number.isFinite(value)) {
return String(value);
}
const cache = formatters[useGrouping ? 1 : 0];
let localeFormatters = cache.get(locale);
if (!localeFormatters) {
localeFormatters = new Map();
cache.set(locale, localeFormatters);
}
let formatter = localeFormatters.get(digitsInfo);
if (!formatter) {
formatter = createFormatter(digitsInfo, locale, useGrouping);
localeFormatters.set(digitsInfo, formatter);
}
return formatter.format(value);
}Test file
ts
import { formatNumber } from './format-number';
describe(formatNumber, () => {
it.for([
[Math.PI, '1.0-2', '3.14'],
[5, '3.0-2', '005'],
[1234.5, '1.2-2', '1234.50'],
[1_234_567.891, '1.0-2', '1234567.89'],
[0.5, '1.0-0', '1'],
[-0.001, '1.0-2', '0'],
[1.2, '1.5-5', '1.20000'],
] as const)('formats %s with %s as %s', ([value, digitsInfo, expected]) => {
expect(formatNumber(value, digitsInfo, 'en-US')).toBe(expected);
});
it('uses the separators and grouping of the locale when grouping', () => {
expect(formatNumber(1_234_567.891, '1.0-2', 'en-US', true)).toBe('1,234,567.89');
expect(formatNumber(1234.5, '1.2-2', 'fr-FR', true)).toBe('1\u{202F}234,50');
expect(formatNumber(1234.5, '1.2-2', 'de-DE', true)).toBe('1.234,50');
expect(formatNumber(1234.5, '1.2-2', 'de-DE', false)).toBe('1234,50');
});
it('takes 1.0-3, en-US and no grouping', () => {
expect(formatNumber(1234.5678)).toBe('1234.568');
});
it('formats NaN and infinities like String()', () => {
expect(formatNumber(NaN, '1.0-2', 'en-US')).toBe('NaN');
expect(formatNumber(-Infinity, '1.0-2', 'en-US')).toBe('-Infinity');
});
it('returns the same output from the cached formatter', () => {
expect(formatNumber(1.5, '1.1-1', 'en-US')).toBe('1.5');
expect(formatNumber(2.25, '1.1-1', 'en-US')).toBe('2.3');
});
it.for(['', '1', '1-2', '.1-1', '2.', '1.5', 'a.0-2'])('throws a TypeError for digitsInfo %j', (digitsInfo) => {
expect(() => formatNumber(1, digitsInfo)).toThrow(TypeError);
});
});Keywords: format, number, formatting, function