Skip to content

@absoluteui/helpers/number

Exact rounding, clamping, percentages, locale-aware parsing and formatting, bytes, ordinals, and number words.

import { ... } from '@absoluteui/helpers/number';

Rounding, percentages, parsing, and locale-aware formatting for everyday numbers such as KPIs, hours, file sizes, and user input. Formatting goes through cached Intl.NumberFormat instances; parsing accepts each locale’s own separators and digits. For money that must add up exactly, use helpers/decimal; the Internationalization guide covers locales.

How do I show a KPI with its change against last period?

Section titled “How do I show a KPI with its change against last period?”

percentChange returns undefined instead of Infinity when the base is zero; the formatters render the value compactly and in the reader’s locale.

import { formatCompact, formatCurrency, formatPercent, percentChange } from '@absoluteui/helpers/number';
const payrollCost = { last: 1_180_000, now: 1_246_500 };
const change = percentChange(payrollCost.last, payrollCost.now, { places: 1 });
change; // => 5.6
formatPercent((change ?? 0) / 100, { locale: 'en-US', maximumFractionDigits: 1, signDisplay: 'always' }); // => '+5.6%'
formatCompact(payrollCost.now, { locale: 'en-US' }); // => '1.2M'
formatCurrency(payrollCost.now, 'INR', { locale: 'en-IN' }); // => '₹12,46,500.00'
percentChange(0, 12); // => undefined

How do I read a number the user typed in their own format?

Section titled “How do I read a number the user typed in their own format?”

parseNumber takes the locale’s group and decimal symbols; strict: true rejects misplaced separators that would otherwise be read as a different number.

import { parseNumber } from '@absoluteui/helpers/number';
parseNumber('1.234,5', { locale: 'de' }); // => 1234.5
parseNumber('1,234.5', { locale: 'en' }); // => 1234.5
parseNumber('12 %', { locale: 'fr' }); // => 0.12
parseNumber('1,2,3', { locale: 'en', strict: true }); // => undefined
parseNumber('twelve', { locale: 'en' }); // => undefined

How do I round logged hours to quarter hours and cap them?

Section titled “How do I round logged hours to quarter hours and cap them?”

roundTo rounds to any step (without float drift), clamp keeps the value in range, and round takes a number of decimal places.

import { clamp, round, roundTo } from '@absoluteui/helpers/number';
const logged = [7.9, 8.13, 13.4];
logged.map((h) => clamp(roundTo(h, 0.25), 0, 12)); // => [8, 8.25, 12]
round(1.005, 2); // => 1.01
Math.round(1.005 * 100) / 100; // => 1 // float error

How do I enforce and show a file size limit?

Section titled “How do I enforce and show a file size limit?”

parseBytes reads limits from configuration ('10 MB', '1 KiB'); formatBytes shows sizes to users, with style: 'long' for screen-reader text.

import { formatBytes, parseBytes } from '@absoluteui/helpers/number';
const limit = parseBytes('10 MB') ?? 0;
const upload = 12_400_000;
upload > limit; // => true
`${formatBytes(upload, { locale: 'en-US' })} exceeds the ${formatBytes(limit, { locale: 'en-US' })} limit`; // => '12.4 MB exceeds the 10 MB limit'
formatBytes(upload, { locale: 'en-US', style: 'long' }); // => '12.4 megabytes'

round rounds halves away from zero; use roundHalfEven (banker’s rounding) when regulations require it, and never compare computed floats with ===:

import { approximatelyEqual, round, roundHalfEven } from '@absoluteui/helpers/number';
[round(2.5), roundHalfEven(2.5), roundHalfEven(3.5)]; // => [3, 2, 4]
0.1 + 0.2 === 0.3; // => false
approximatelyEqual(0.1 + 0.2, 0.3); // => true

approximatelyEqual

Tolerant equality: |a - b| <= max(relative x max(|a|, |b|), absolute). NaN is never equal.

function approximatelyEqual(a: number, b: number, options?: ApproxOptions): boolean;

Example:

approximatelyEqual(0.1 + 0.2, 0.3) // => true

ceil

Rounds toward +infinity to places decimals, exact for decimal inputs.

function ceil(value: number, places?: number): number;

Example:

ceil(1.01, 1) // => 1.1

clamp

Restricts value to [min, max]. Throws HLP_NUMBER_RANGE when min > max.

function clamp(value: number, min: number, max: number): number;

Example:

clamp(15, 0, 10) // => 10

factorial

Exact factorial as bigint; n must be an integer in 0..MAX_FACTORIAL.

function factorial(n: number): bigint;

Example:

factorial(20) // => 2432902008176640000n

floor

Rounds toward -infinity to places decimals, exact for decimal inputs.

function floor(value: number, places?: number): number;

Example:

floor(1.99, 1) // => 1.9

gcd

Greatest common divisor of two integers (always >= 0).

function gcd(a: number, b: number): number;

Example:

gcd(12, -18) // => 6

gcdBigInt

gcd for bigint.

function gcdBigInt(a: bigint, b: bigint): bigint;

Example:

gcdBigInt(12n, 18n) // => 6n

inRange

True when value lies between min and max (bounds may be given in either order).

function inRange(value: number, min: number, max: number, options?: InRangeOptions): boolean;

Example:

inRange(10, 0, 10, { inclusive: 'min' }) // => false

inverseLerp

Inverse of lerp: where value sits between a and b (0 when a === b).

function inverseLerp(a: number, b: number, value: number): number;

Example:

inverseLerp(10, 20, 15) // => 0.5

isEven

True for even integers.

function isEven(value: number): boolean;

Example:

isEven(-4) // => true

isNegativeZero

True only for -0.

function isNegativeZero(value: number): boolean;

Example:

isNegativeZero(-0) // => true

isOdd

True for odd integers.

function isOdd(value: number): boolean;

Example:

isOdd(-3) // => true

isPrime

Deterministic primality test for safe integers and for bigint below 3.3 × 10²⁴ (trial division for small values, Miller–Rabin with fixed bases otherwise).

function isPrime(n: number | bigint): boolean;

Example:

isPrime(2_147_483_647) // => true

kahanSum

Compensated (Kahan-Babuška-Neumaier) sum; far less rounding error than a plain loop.

function kahanSum(values: Iterable<number>): number;

Example:

kahanSum([0.1, 0.2, 0.3]) // => 0.6 // plain reduce gives 0.6000000000000001

lcm

Least common multiple of two integers; throws HLP_NUMBER_UNSAFE when the result is not a safe integer.

function lcm(a: number, b: number): number;

Example:

lcm(4, 6) // => 12

lcmBigInt

lcm for bigint.

function lcmBigInt(a: bigint, b: bigint): bigint;

Example:

lcmBigInt(4n, 6n) // => 12n

lerp

Linear interpolation; exact at t = 0 and t = 1.

function lerp(a: number, b: number, t: number): number;

Example:

lerp(10, 20, 0.25) // => 12.5

normalizeZero

Converts -0 to 0; other values pass through.

function normalizeZero(value: number): number;

Example:

normalizeZero(-0) // => 0

percent

part as a percentage of total; undefined when total is 0 or inputs are not finite.

function percent(part: number, total: number, options?: PercentOptions): number | undefined;

Example:

percent(1, 3, { places: 2 }) // => 33.33

percentChange

Percentage change from from to to relative to |from|; undefined when from is 0.

function percentChange(from: number, to: number, options?: PercentOptions): number | undefined;

Example:

percentChange(80, 100) // => 25

product

Product of all values. Empty input gives 1.

function product(values: Iterable<number>): number;

Example:

product([2, 3, 4]) // => 24

ratio

a / b, or undefined when b is 0 or the result is not finite.

function ratio(a: number, b: number, options?: PercentOptions): number | undefined;

Example:

ratio(3, 4) // => 0.75

remap

Maps value from [inMin, inMax] to [outMin, outMax].

function remap(value: number, inMin: number, inMax: number, outMin: number, outMax: number, options?: RemapOptions): number;

Example:

remap(5, 0, 10, 0, 100) // => 50

round

Rounds half away from zero to places decimals, exact for decimal inputs (Excel ROUND). Negative places round to tens, hundreds, …

function round(value: number, places?: number): number;

Example:

round(1.005, 2); // => 1.01 // naive Math.round(1.005 * 100) / 100 gives 1

roundHalfEven

Rounds half to even (“banker’s rounding”) to places decimals, exact for decimal inputs.

function roundHalfEven(value: number, places?: number): number;

Example:

roundHalfEven(2.5); // => 2
roundHalfEven(0.125, 2); // => 0.12

roundTo

Rounds to the nearest multiple of step (half away from zero), without float drift.

function roundTo(value: number, step: number): number;

Example:

roundTo(1.13, 0.25); // => 1.25
roundTo(0.125, 0.05); // => 0.15

sign

Sign as -1 | 0 | 1 (-0 gives 0), or NaN for NaN.

function sign(value: number): number;

Example:

sign(-0) // => 0

sum

Compensated sum (alias of kahanSum). Empty input gives 0.

function sum(values: Iterable<number>): number;

Example:

sum([1, 2, 3]) // => 6

toFixedSafe

Plain decimal string with no exponent notation. With places, rounds half away from zero on the exact decimal value (unlike Number#toFixed) and pads to places digits. -0 and negative values that round to zero print without a minus sign.

function toFixedSafe(value: number, places?: number): string;

Example:

toFixedSafe(1e21); // => '1000000000000000000000'
toFixedSafe(1.005, 2); // => '1.01'

trunc

Drops digits beyond places decimals (rounds toward zero). Named trunc to avoid clashing with the string module’s truncate.

function trunc(value: number, places?: number): number;

Example:

trunc(-1.29, 1) // => -1.2

MAX_FACTORIAL

Largest accepted factorial argument.

const MAX_FACTORIAL = 10000;

ApproxOptions

interface ApproxOptions {
readonly relative?: number;
readonly absolute?: number;
}

InRangeOptions

interface InRangeOptions {
readonly inclusive?: boolean | 'min' | 'max';
}

PercentOptions

interface PercentOptions {
readonly places?: number;
}

RemapOptions

interface RemapOptions {
readonly clamp?: boolean;
}

formatBytes

Human-readable byte size through Intl units.

function formatBytes(bytes: number, options?: FormatBytesOptions): string;

Example:

formatBytes(2_400_000, { locale: 'en-US' }); // => '2.4 MB'
formatBytes(2_400_000, { locale: 'en-US', style: 'long' }); // => '2.4 megabytes'

formatCompact

Compact notation.

function formatCompact(value: NumericValue, options?: FormatCompactOptions): string;

Example:

formatCompact(1234, { locale: 'en-US' }); // => '1.2K'
formatCompact(1234, { locale: 'en-US', display: 'long' }); // => '1.2 thousand'

formatCurrency

Formats money in an ISO 4217 currency.

function formatCurrency(value: NumericValue, currency: string, options?: FormatCurrencyOptions): string;

Example:

formatCurrency(1234.5, 'EUR', { locale: 'fr-FR' }) // => '1\u202f234,50\u00a0€'

formatNumber

Intl.NumberFormat with a cached formatter per locale and options. Decimal strings format without float loss.

function formatNumber(value: NumericValue, options?: FormatNumberOptions): string;

Example:

formatNumber(1234.5, { locale: 'de-DE' }) // => '1.234,5'

formatOrdinal

Ordinal number (“1st”, “2nd”). The category comes from Intl.PluralRules({ type: 'ordinal' }); suffixes are built in for en, fr, de, nl, es, it, pt, sv, da, nb, fi, pl, cs. Other languages (whose ordinals depend on grammatical gender or case) return the plain number unless suffixes is given.

function formatOrdinal(n: number, localeOrOptions?: string | FormatOrdinalOptions): string;

Example:

formatOrdinal(22, 'en'); // => '22nd'
formatOrdinal(1, 'fr'); // => '1er'

formatPercent

Formats a ratio as a percentage (0.256 -> “26%”).

function formatPercent(value: number, options?: Omit<FormatNumberOptions, 'style'>): string;

Example:

formatPercent(0.256, { maximumFractionDigits: 1 }) // => '25.6%'

formatUnit

Formats with an ECMA-402 sanctioned unit (or a -per- compound such as kilometer-per-hour).

function formatUnit(value: NumericValue, unit: string, options?: FormatUnitOptions): string;

Example:

formatUnit(5, 'kilometer', { unitDisplay: 'long' }) // => '5 kilometers'

FormatBytesOptions

interface FormatBytesOptions {
readonly locale?: LocaleInput;
readonly base?: 1000 | 1024;
readonly style?: 'short' | 'narrow' | 'long';
readonly maximumFractionDigits?: number;
}

FormatCompactOptions

interface FormatCompactOptions extends Omit<FormatNumberOptions, 'notation' | 'compactDisplay'> {
readonly display?: 'short' | 'long';
}

FormatCurrencyOptions

interface FormatCurrencyOptions extends Omit<FormatNumberOptions, 'style' | 'currency' | 'currencyDisplay'> {
readonly display?: 'symbol' | 'narrowSymbol' | 'code' | 'name';
}

FormatNumberOptions

interface FormatNumberOptions extends Intl.NumberFormatOptions {
readonly locale?: LocaleInput;
}

FormatOrdinalOptions

interface FormatOrdinalOptions {
readonly locale?: LocaleInput;
readonly suffixes?: Partial<Record<PluralCategory, string>>;
}

FormatUnitOptions

interface FormatUnitOptions extends Omit<FormatNumberOptions, 'style' | 'unit' | 'unitDisplay'> {
readonly unitDisplay?: 'short' | 'narrow' | 'long';
}

LocaleInput

A BCP 47 locale tag or a list of them; undefined uses the runtime default.

type LocaleInput = string | readonly string[] | undefined;

NumericValue

Values Intl.NumberFormat formats exactly, including decimal strings such as '0.1'.

type NumericValue = number | bigint | Intl.StringNumericLiteral;

isSafeNumberString

True when text is a plain decimal string (-12.5, .5, no exponent) whose value a number holds exactly: integers must be safe, fractions at most 15 significant digits.

function isSafeNumberString(text: string): boolean;

Example:

isSafeNumberString('9007199254740993'); // => false
isSafeNumberString('0.1'); // => true

parseBytes

Parses a byte size (“1.5 GB”, “512 KiB”, “3 megabytes”, “42”) into whole bytes; undefined when invalid or negative.

function parseBytes(text: string, options?: ParseBytesOptions): number | undefined;

Example:

parseBytes('1.5 GB'); // => 1500000000
parseBytes('1 KiB'); // => 1024

parseNumber

Locale-aware number parse; undefined for garbage, empty text, or values outside the double range.

function parseNumber(text: string, options?: ParseNumberOptions): number | undefined;

Example:

parseNumber('١٢٣٫٥', { locale: 'ar-EG' }); // => 123.5
parseNumber('12 %', { locale: 'fr' }); // => 0.12

parseNumberString

Parses localized numeric text into a canonical ASCII decimal string (lossless, no exponent), or undefined when the text is not a number. A percent sign divides by 100. Non-Latin digits (Arabic-Indic, Devanagari, …), NBSP / narrow NBSP group separators and U+2212 minus are accepted. Throws HLP_NUMBER_TOO_LONG above MAX_NUMBER_TEXT characters.

function parseNumberString(text: string, options?: ParseNumberOptions): string | undefined;

Example:

parseNumberString('1.234.567,891', { locale: 'de' }) // => '1234567.891'

MAX_NUMBER_TEXT

Longest accepted input for parseNumber / parseNumberString.

const MAX_NUMBER_TEXT = 1000;

ParseBytesOptions

interface ParseBytesOptions {
readonly base?: 1000 | 1024;
readonly locale?: LocaleInput;
}

ParseNumberOptions

interface ParseNumberOptions {
readonly locale?: LocaleInput;
readonly strict?: boolean;
}

fromRoman

Parses a canonical Roman numeral (case-insensitive, 1..3999); undefined for non-canonical forms like “IIII”.

function fromRoman(text: string): number | undefined;

Example:

fromRoman('mcmxcix') // => 1999

numberToWords

English words for a number up to +/-10¹⁵ (English only, by design). Fractions are read digit by digit.

function numberToWords(value: number, options?: NumberToWordsOptions): string;

Example:

numberToWords(-1234.05) // => 'minus one thousand two hundred thirty-four point zero five'

toRoman

Roman numeral for an integer in 1..3999.

function toRoman(value: number): string;

Example:

toRoman(2026) // => 'MMXXVI'

NumberToWordsOptions

interface NumberToWordsOptions {
readonly and?: boolean;
}