@absoluteui/helpers/number
Exact rounding, clamping, percentages, locale-aware parsing and formatting, bytes, ordinals, and number words.
import { ... } from '@absoluteui/helpers/number';How to use
Section titled “How to use”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.6formatPercent((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); // => undefinedHow 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.5parseNumber('1,234.5', { locale: 'en' }); // => 1234.5parseNumber('12 %', { locale: 'fr' }); // => 0.12parseNumber('1,2,3', { locale: 'en', strict: true }); // => undefinedparseNumber('twelve', { locale: 'en' }); // => undefinedHow 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.01Math.round(1.005 * 100) / 100; // => 1 // float errorHow 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'What goes wrong?
Section titled “What goes wrong?”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; // => falseapproximatelyEqual(0.1 + 0.2, 0.3); // => trueAPI reference
Section titled “API reference”Functions
Section titled “Functions”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) // => trueceil
Rounds toward +infinity to places decimals, exact for decimal inputs.
function ceil(value: number, places?: number): number;Example:
ceil(1.01, 1) // => 1.1clamp
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) // => 10factorial
Exact factorial as bigint; n must be an integer in 0..MAX_FACTORIAL.
function factorial(n: number): bigint;Example:
factorial(20) // => 2432902008176640000nfloor
Rounds toward -infinity to places decimals, exact for decimal inputs.
function floor(value: number, places?: number): number;Example:
floor(1.99, 1) // => 1.9gcd
Greatest common divisor of two integers (always >= 0).
function gcd(a: number, b: number): number;Example:
gcd(12, -18) // => 6gcdBigInt
gcd for bigint.
function gcdBigInt(a: bigint, b: bigint): bigint;Example:
gcdBigInt(12n, 18n) // => 6ninRange
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' }) // => falseinverseLerp
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.5isEven
True for even integers.
function isEven(value: number): boolean;Example:
isEven(-4) // => trueisNegativeZero
True only for -0.
function isNegativeZero(value: number): boolean;Example:
isNegativeZero(-0) // => trueisOdd
True for odd integers.
function isOdd(value: number): boolean;Example:
isOdd(-3) // => trueisPrime
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) // => truekahanSum
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.6000000000000001lcm
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) // => 12lcmBigInt
lcm for bigint.
function lcmBigInt(a: bigint, b: bigint): bigint;Example:
lcmBigInt(4n, 6n) // => 12nlerp
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.5normalizeZero
Converts -0 to 0; other values pass through.
function normalizeZero(value: number): number;Example:
normalizeZero(-0) // => 0percent
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.33percentChange
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) // => 25product
Product of all values. Empty input gives 1.
function product(values: Iterable<number>): number;Example:
product([2, 3, 4]) // => 24ratio
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.75remap
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) // => 50round
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 1roundHalfEven
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); // => 2roundHalfEven(0.125, 2); // => 0.12roundTo
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.25roundTo(0.125, 0.05); // => 0.15sign
Sign as -1 | 0 | 1 (-0 gives 0), or NaN for NaN.
function sign(value: number): number;Example:
sign(-0) // => 0sum
Compensated sum (alias of kahanSum). Empty input gives 0.
function sum(values: Iterable<number>): number;Example:
sum([1, 2, 3]) // => 6toFixedSafe
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.2Constants
Section titled “Constants”MAX_FACTORIAL
Largest accepted factorial argument.
const MAX_FACTORIAL = 10000;Interfaces
Section titled “Interfaces”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;}Format
Section titled “Format”Functions
Section titled “Functions”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'Interfaces
Section titled “Interfaces”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;Functions
Section titled “Functions”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'); // => falseisSafeNumberString('0.1'); // => trueparseBytes
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'); // => 1500000000parseBytes('1 KiB'); // => 1024parseNumber
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.5parseNumber('12 %', { locale: 'fr' }); // => 0.12parseNumberString
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'Constants
Section titled “Constants”MAX_NUMBER_TEXT
Longest accepted input for parseNumber / parseNumberString.
const MAX_NUMBER_TEXT = 1000;Interfaces
Section titled “Interfaces”ParseBytesOptions
interface ParseBytesOptions { readonly base?: 1000 | 1024; readonly locale?: LocaleInput;}ParseNumberOptions
interface ParseNumberOptions { readonly locale?: LocaleInput; readonly strict?: boolean;}Functions
Section titled “Functions”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') // => 1999numberToWords
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'Interfaces
Section titled “Interfaces”NumberToWordsOptions
interface NumberToWordsOptions { readonly and?: boolean;}