Skip to content

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): string

Parameters ​

NameDescription
valueThe number to format.
digitsInfo'{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}', such as '1.0-2'. Defaults to '1.0-3'.
localeThe BCP 47 locale, such as 'fr-FR'. Defaults to 'en-US'.
useGroupingWhether to separate the thousands. Defaults to false.

Returns ​

The formatted number.

Errors ​

  • Throws TypeError: When digitsInfo does 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

Generated from the JSDoc of the sources.