anyamount API reference

Overview

anyamount is a tiny number formatter built entirely on the native Intl.NumberFormat browser API. One function, one options object, three modes — plus a helper for bare currency symbols. The 1.x API is stable: new options arrive in minors, breaking changes only in majors.

The browser already knows how to format numbers, money, and units in 200+ languages. anyamount just makes that API pleasant to use.

import { anyamount } from 'anyamount'

anyamount(1234567)
// "1.2M"  — smart mode (default)

anyamount(1999, { mode: 'currency', currency: 'EUR' })
// "€1,999.00"

anyamount(3.2, { mode: 'unit', unit: 'gigabyte' })
// "3.2 GB"

Install

npm install anyamount
# or
pnpm add anyamount
# or
yarn add anyamount

Or take the whole family at once with npm install anyfamily.

anyamount()

The single entry point. Pass a number, optionally pass options.

anyamount(value)
anyamount(value, options?)

anyamount(1234567)
// runtime locale, smart mode

anyamount(9999, { locale: 'en' })
// "9,999"  — below the compact cutoff

anyamount(1234567, { locale: 'en', style: 'long' })
// "1.2 million"

anyamount(1999.99, { mode: 'currency', currency: 'EUR', locale: 'en', digits: 0 })
// "€2,000"

Migrating from 1.x

2.0 removed the separate anyamountPartsand the other extra exports — they are the same functions and values, reached through the one name the package exports.

- import { anyamount, anyamountParts, anyamountSymbol } from 'anyamount'
+ import { anyamount } from 'anyamount'

- anyamountParts(1999, opts)
+ anyamount.parts(1999, opts)

- anyamountSymbol('USD')
+ anyamount.symbol('USD')

Arguments, return values and throwing behaviour are unchanged, and nothing else in the API moved. Every any* package follows this shape from 2.0 on: the bare call does the job, everything else hangs off the same name.

anyamount.parts()

Same arguments as anyamount(), but returns the Intl.NumberFormat.formatToParts output unchanged — style the number apart from the currency symbol or unit, or rebuild the output your own way.

import { anyamount } from 'anyamount'

anyamount.parts(1999, { mode: 'currency', currency: 'EUR', locale: 'en' })
// [
//   { type: 'currency', value: '€' },
//   { type: 'integer', value: '1' },
//   { type: 'group', value: ',' },
//   { type: 'integer', value: '999' },
//   { type: 'decimal', value: '.' },
//   { type: 'fraction', value: '00' },
// ]

// React: shrink the currency symbol
anyamount.parts(price, { mode: 'currency', currency: 'EUR' }).map((p, i) =>
  p.type === 'currency' ? <small key={i}>{p.value}</small> : p.value,
)

Note: part values keep the original Intl characters — the space between number and unit can be U+00A0 or U+202F (no-break spaces) depending on locale and ICU version.

anyamount.symbol()

Resolves an ISO 4217 code to its localized symbol, with no number attached — for labels, currency pickers, and input affixes, where the amount is rendered separately (or not at all).

import { anyamount } from 'anyamount'

anyamount.symbol('USD', { locale: 'en' })   // "$"
anyamount.symbol('EUR', { locale: 'en' })   // "€"
anyamount.symbol('GBP', { locale: 'en' })   // "£"
anyamount.symbol('JPY', { locale: 'ja' })   // "¥"
anyamount.symbol('RUB', { locale: 'ru' })   // "₽"

anyamount.symbol('USD', { locale: 'en', display: 'code' })   // "USD"
anyamount.symbol('USD', { locale: 'en', display: 'name' })   // "US dollars"

display defaults to 'narrowSymbol' — the bare symbol, never the disambiguated US$some locales prefer. Codes with no symbol in the locale's data come back as the code itself.

Note: a malformed code throws a RangeError straight from Intl — 'US' is not a currency. Formatting a full amount? Stay in currency mode with currencyDisplay; this is the escape hatch for when there is no amount.

anyamount.range()

Two numbers as one range, the way the locale writes it — the shared parts collapse, so a price band or a weight bracket reads as one thing. Same options as the plain call; both ends are validated the same way, and smart mode picks compact notation from the bigger end.

import { anyamount } from 'anyamount'

anyamount.range(10, 20, { mode: 'currency', currency: 'EUR', locale: 'en' })  // "€10.00 – 20.00"
anyamount.range(1, 2.5, { mode: 'unit', unit: 'kilogram', locale: 'en' })      // "1–2.5 kg"
anyamount.range(1500, 2400, { compact: true, locale: 'en' })                   // "1.5K – 2.4K"
anyamount.range(3, 7, { locale: 'de' })                                        // "3–7"

Built on Intl.NumberFormat.formatRange (ES2023). Where the runtime lacks it — Node 18 — the two ends are formatted separately and joined with an en dash, so the call never throws for that reason.

anyamount.parse()

The other direction: the text a person typed, in their locale, back to a number. For amount, price and quantity inputs — where Number("1.999,00") is NaN and parseFloat stops at the first comma.

import { anyamount } from 'anyamount'

anyamount.parse('1.999,00', { locale: 'de' })    // 1999
anyamount.parse('1,999.00', { locale: 'en' })    // 1999
anyamount.parse('1 234,5', { locale: 'fr' })     // 1234.5
anyamount.parse('١٬٢٣٤٫٥', { locale: 'ar-EG' })   // 1234.5

anyamount.parse('€1,999.00', { locale: 'en' })   // 1999   — what wraps the number is ignored
anyamount.parse('1 234,5 kr', { locale: 'sv' })  // 1234.5
anyamount.parse('12%', { locale: 'en' })         // 12
anyamount.parse('(1,999.00)', { locale: 'en' })  // -1999  — accounting parentheses
anyamount.parse('−42', { locale: 'sv' })         // -42    — the locale's own minus

anyamount.parse('abc', { locale: 'en' })         // NaN
anyamount.parse('12abc34', { locale: 'en' })     // NaN    — letters between digits
anyamount.parse('1.2.3', { locale: 'en' })       // NaN    — two decimal points

Everything is read off Intl: the locale's group and decimal separators, its minus sign and its digits — every numbering system, no tables. The locale's group separator is skipped, its decimal separator is the decimal, ASCII digits are always accepted. A minus on either side, or accounting parentheses, makes the result negative; two signs make it NaN.

One rule beyond the locale, because people type what they mean: a separator that appears once and is followed by one or two digits is a decimal point, whatever the locale says — '1.5'in a German form is one and a half, not fifteen hundred. Three digits keep the locale's reading: '1.500' is fifteen hundred in German and one and a half in English.

anyamount.parse('1.5', { locale: 'de' })     // 1.5
anyamount.parse('1.500', { locale: 'de' })   // 1500
anyamount.parse('1,50', { locale: 'en' })    // 1.5
anyamount.parse('1,500', { locale: 'en' })   // 1500

Returns NaN rather than throwing: unparseable input is the normal case for a text field, not an error. Compact suffixes ('1.2K') are not read — that would be guessing. The only throw is a TypeError for a non-string.

// A price field: format on blur, parse on change
const [text, setText] = useState(anyamount(price, { mode: 'currency', currency, locale }))

<input
  value={text}
  inputMode="decimal"
  onChange={(e) => {
    setText(e.target.value)
    const n = anyamount.parse(e.target.value, { locale })
    if (!Number.isNaN(n)) onChange(n)
  }}
  onBlur={() => setText(anyamount(price, { mode: 'currency', currency, locale }))}
/>

Modes

The mode option picks the rendering strategy. Each mode reads only the options that apply to it — the rest are ignored.

smart (default)

Compact notation for big numbers, plain formatting for small ones. The cutoff is |value| >= 10000.

1234567→ "1.2M"
10000→ "10K"
9999→ "9,999"
42→ "42"
0.1234→ "0.12"

reads: locale, style, digits

currency

Money via the Intl.NumberFormat currency style. currency is required — any ISO 4217 code. Missing it throws a TypeError.

anyamount(1999, { mode: 'currency', currency: 'EUR', locale: 'en' })
// "€1,999.00"

anyamount(1999, { mode: 'currency', currency: 'RSD', locale: 'sr' })
// "1.999,00 RSD"

anyamount(1999, { mode: 'currency', currency: 'JPY', locale: 'ja' })
// "¥1,999"  — JPY has no minor unit, Intl knows

anyamount(1999.99, { mode: 'currency', currency: 'EUR', locale: 'en', digits: 0 })
// "€2,000"

currencyDisplay picks how the currency itself is spelled — symbol by default, opt into anything else.

anyamount(1999, { mode: 'currency', currency: 'USD', locale: 'en' })
// "$1,999.00"  — 'symbol' (default)

anyamount(1999, { mode: 'currency', currency: 'USD', locale: 'en-CA', currencyDisplay: 'narrowSymbol' })
// "$1,999.00"  — bare symbol, where the locale would print "US$"

anyamount(1999, { mode: 'currency', currency: 'USD', locale: 'en', currencyDisplay: 'code' })
// "USD 1,999.00"

anyamount(1999, { mode: 'currency', currency: 'USD', locale: 'en', currencyDisplay: 'name' })
// "1,999.00 US dollars"

reads: locale, currency, currencyDisplay, digits

unit

Measurements via the Intl.NumberFormat unit style. unit is required — any sanctioned identifier, including compound -per- pairs. Missing it throws a TypeError.

anyamount(3.2, { mode: 'unit', unit: 'gigabyte', locale: 'en' })
// "3.2 GB"

anyamount(120, { mode: 'unit', unit: 'kilometer-per-hour', locale: 'en' })
// "120 km/h"

anyamount(3.2, { mode: 'unit', unit: 'gigabyte', locale: 'en', style: 'long' })
// "3.2 gigabytes"

anyamount(5, { mode: 'unit', unit: 'kilometer', locale: 'en', style: 'narrow' })
// "5km"

reads: locale, unit, style, digits

Units

Intl supports a fixed, sanctioned list of unit identifiers (from ECMA-402), plus any <unit>-per-<unit> compound of them. anyamount ships the full list as a TypeScript union, so invalid units fail at compile time.

acre bit byte celsius centimeter day degree fahrenheit
fluid-ounce foot gallon gigabit gigabyte gram hectare hour
inch kilobit kilobyte kilogram kilometer liter megabit
megabyte meter microsecond mile mile-scandinavian milliliter
millimeter millisecond minute month nanosecond ounce percent
petabyte pound second stone terabit terabyte week yard year
// compounds work too
anyamount(120, { mode: 'unit', unit: 'kilometer-per-hour' })   // "120 km/h"
anyamount(8.5, { mode: 'unit', unit: 'liter-per-kilometer' })  // "8.5 L/km"
anyamount(2, { mode: 'unit', unit: 'meter-per-second' })       // "2 m/s"

Options

mode'smart' | 'currency' | 'unit'default: 'smart'

Rendering strategy. Each mode reads only the options that apply to it.

localestring | string[]default: runtime locale

Any valid BCP 47 locale tag, or a fallback array — 'en', 'en-US', 'zh-TW', ['sr-Latn-RS', 'en'].

currencystring

Currency mode only, required. Any ISO 4217 code — 'EUR', 'USD', 'JPY', 'RSD'.

currencyDisplay'symbol' | 'narrowSymbol' | 'code' | 'name'default: 'symbol'

Currency mode only. How the currency is spelled: '$1,999.00', 'USD 1,999.00', or '1,999.00 US dollars'. 'narrowSymbol' keeps the bare '$' where a locale would print 'US$'.

unitUnit

Unit mode only, required. A sanctioned unit identifier or a compound '<unit>-per-<unit>' pair. Typed as a union — your editor autocompletes it.

style'long' | 'short' | 'narrow'default: 'short'

Smart and unit modes. Wording length: '1.2M' vs '1.2 million', '3.2 GB' vs '3.2 gigabytes'.

compactboolean | numberdefault: 10000

Smart mode only. When compact notation ('1.2K', '3.4M') kicks in: true — always, for counters and badges; false — never; a number — from that absolute value up. The default keeps 9,999 plain and turns 12,345 into '12.3K'.

digitsnumberdefault: per mode

maximumFractionDigits — a ceiling, not a fixed width: trailing zeros are not padded on, so digits: 2 renders 2.5, not 2.50. Defaults: smart — 2 plain / 1 compact, unit — 2, currency — the currency's own (which it keeps as a minimum).

What breaks without this

Every one of these is an assumption baked into a hand-written formatter, and each is wrong somewhere.

The symbol is not always in front

'$' + value.toFixed(2) is a German price written backwards: de-DE puts the symbol last and swaps both separators — 1.999,00 €. Two locales sharing a currency need not write it the same way.

toFixed is not money rounding

It rounds a binary double, so the half-way cases go where the bits fall rather than where accounting expects. It also assumes two decimals, which JPY does not have and KWD exceeds — the right number of digits is a fact about the currency, and Intl already knows it.

Compact notation is not K and M

1.2M is English. Russian writes 1,2 млн; Japanese groups by ten-thousands and writes 123万, a different place value, not a translated suffix. A K/M/B table cannot be localized into being correct.

Units are a closed list, and they inflect

Writing "3.2 GB" by hand skips the part where the unit name agrees with the number and the locale. Intl takes a sanctioned list of units and handles both — outside that list there is no localized name to be had, from any library.

Recipes

Copy, paste, move on.

// Dashboard stat
anyamount(views, { locale: 'en' })
// "1.2M"

// …spelled out
anyamount(views, { locale: 'en', style: 'long' })
// "1.2 million"

// Price
anyamount(product.cents / 100, { mode: 'currency', currency: 'EUR', locale: 'de' })
// "1.999,00 €"

// Price with no cents
anyamount(total, { mode: 'currency', currency: 'EUR', digits: 0 })
// "€2,000"

// Storage meter
anyamount(file.gb, { mode: 'unit', unit: 'gigabyte' })
// "3.2 GB"

// Speed, compound unit
anyamount(120, { mode: 'unit', unit: 'kilometer-per-hour', locale: 'ru' })
// "120 км/ч"

// Currency affix inside an input, amount rendered separately
anyamount.symbol(account.currency)
// "$"

// Badge / counter
anyamount(post.likes, { compact: true })
// "1.2K"

// A table that must never abbreviate
anyamount(row.total, { compact: false })
// "15,000"

// Price band
anyamount.range(plan.min, plan.max, { mode: 'currency', currency: 'USD' })
// "$10.00 – 20.00"

// What the user typed, back to a number
anyamount.parse(input.value, { locale })
// 1999.5

React / Next.js

anyamount is pure and synchronous, so it works in a component as-is. Whatanyfamily-react adds is a shared locale: set it once onAnyfamilyProvider and every hook below picks it up, so you do not thread locale through every call.

import { AnyfamilyProvider, useAnyamount } from 'anyfamily-react'

function Price({ cents }: { cents: number }) {
  return <b>{useAnyamount(cents / 100, { mode: 'currency', currency: 'EUR' })}</b>
}

<AnyfamilyProvider locale="de">
  <Price cents={199900} />
</AnyfamilyProvider>

`useAnyamountSymbol` is there too, for the bare currency symbol.

Locales

Same calls in a few languages — no extra setup, no locale files.

// smart mode
anyamount(1234567, { locale: 'ru' })   // "1,2 млн"
anyamount(1234567, { locale: 'de' })   // "1,2 Mio."
anyamount(1234567, { locale: 'ja' })   // "123.5万"

// currency mode
anyamount(1999, { mode: 'currency', currency: 'USD', locale: 'de' })
// "1.999,00 $"
anyamount(1999, { mode: 'currency', currency: 'INR', locale: 'hi' })
// "₹1,999.00"

// unit mode
anyamount(120, { mode: 'unit', unit: 'kilometer-per-hour', locale: 'ru' })
// "120 км/ч"

Pass any valid BCP 47 language tag — including regional variants like en-GB, zh-TW, or pt-BR. Locale is optional; when omitted, native Intl uses the runtime locale. Fallback arrays like ['sr-Latn-RS', 'en'] also work.

Output is pure — no clock reads, no environment sniffing — so server and client render identically. SSR-safe by construction.

vs the alternatives

What you would otherwise reach for, and what changes if you do.

anyamountnumeral.jsaccounting.js
locale data bundlednone (Intl)one file per localenone, you configure it
locales200+registered by handwhatever you pass
currency rulesfrom the currencymanual symbolmanual symbol
decimal digitsper currencymanualmanual
unitssanctioned listnono
compact notationevery localeEnglish formsno
dependencies000

anyamount is 1.6kb gzipped and formats numbers — and reads them back. It is not a money type: it does not add prices, hold exchange rates, or protect you from floating-point arithmetic. Do the arithmetic in minor units or in a decimal library, then hand the result here to be written down.

Compatibility

anyamount uses Intl.NumberFormat with compact notation and unit support — widely available since 2020.

Node.js18+CI runs the suite on Node 20, 22, 24
Chrome77+
Firefox78+
Safari14.1+
Edge79+
Vercel Edge Runtime
Cloudflare Workers
Deno

Limitations

A few things worth knowing before you ship:

No byte auto-scaling yet

anyamount(3200000000, { mode: 'unit', unit: 'byte' }) will not pick GB for you — pass the unit you want. Automatic scaling is planned for a future minor.

Output depends on the runtime's Intl data

anyamount delegates all formatting to native Intl. Exact output — separators, spacing, compact suffixes — may vary between Node versions, browsers, and OSes. Don't hardcode expected strings in tests; use pattern matching instead.

Sanctioned units only

Intl supports a fixed list of unit identifiers and -per- compounds of them. There is no way to format arbitrary custom units — that's an Intl constraint, not an anyamount one.

Deliberately small

One function, three modes, on purpose. No percent mode, no ranges, no parsing. anyamount follows semver — the 1.x API is stable, new options arrive in minors, breaking changes only in majors.