# anyamount — money & numbers
> Formats a number the way the locale writes it — compact ("1.2M"), money ("€1,999.00"), units ("3.2 GB") — and reads a typed amount back into a number.
- wraps: Intl.NumberFormat
- install: npm install anyamount
- size: 1.6 kB gzip, zero dependencies, ESM + CJS, TypeScript types included
- runtime: Node 18+, every modern browser, Deno, Bun, edge runtimes.
- docs: https://anyfamily.site/docs/anyamount
- demo: https://anyfamily.site/anyamount
- playground: https://stackblitz.com/github/kirilinsky/anyfamily/tree/main/examples/vanilla?file=src/main.ts
- npm: https://www.npmjs.com/package/anyamount
- source: https://github.com/kirilinsky/anyfamily/tree/main/packages/anyamount
- family: https://anyfamily.site/llms.txt
## why
toFixed and hand-placed separators break outside en-US; numeral.js is 20 kB and English-first. anyamount is one call over the formatter the runtime already has — any currency, any sanctioned unit, bigint — plus parse() for the input field, the direction Intl never covered.
## usage
```ts
anyamount(1234567) // "1.2M"
anyamount(1999, { mode: "currency", currency: "EUR" }) // "€1,999.00"
anyamount(3.2, { mode: "unit", unit: "gigabyte" }) // "3.2 GB"
anyamount(1234, { compact: true }) // "1.2K" — badges; compact: false never abbreviates
anyamount.range(10, 20, { mode: "currency", currency: "EUR" }) // "€10.00 – 20.00"
anyamount.parse("1.999,50", { locale: "de" }) // 1999.5 — NaN when it is not a number
anyamount.symbol("USD") // "$"
```
## not for
Money arithmetic, exchange rates, decimal precision — do the maths in minor units or a decimal library, then format the result here.
---
# anyamount
Tiny human-readable number formatter built on native Intl. Turn numbers into "1.2M", "€1,999.00", "3.2 GB", or "120 км/ч".
▸ live demo · ▸ full docs · ▸ any family
---
**One export. Three modes, a range, and parse. Any locale. ~1.6kb gzip. Zero dependencies.**
`Intl.NumberFormat` is powerful. anyamount makes it usable. Built for
dashboards, pricing, storage meters and stats — anywhere a raw number should
read like a person wrote it. No locale files, no plugins, no config.
```ts
import { anyamount } from "anyamount";
anyamount(1234567); // "1.2M" — smart (default)
anyamount(1999, { mode: "currency", currency: "EUR" }); // "€1,999.00"
anyamount(3.2, { mode: "unit", unit: "gigabyte" }); // "3.2 GB"
anyamount(1234567, { locale: "ru" }); // "1,2 млн"
anyamount.symbol("USD"); // "$"
```
---
## install
```bash
npm install anyamount
```
---
## usage
```ts
anyamount(value);
anyamount(value, options);
```
`value` is a `number` or a `bigint`.
`anyamount.parts()` takes the same arguments and returns the
`Intl.NumberFormat.formatToParts` output — style the number apart from the
currency symbol or unit.
```tsx
anyamount.parts(1999, { mode: "currency", currency: "EUR", locale: "en" });
// [{ type: "currency", value: "€" }, { type: "integer", value: "1" }, …]
anyamount.parts(price, { mode: "currency", currency: "EUR" }).map((p, i) =>
p.type === "currency" ? {p.value} : p.value,
);
```
`anyamount.symbol()` takes a **currency code** rather than an amount, and
returns the bare symbol — for labels, currency pickers and input affixes.
```ts
anyamount.symbol("USD", { locale: "en" }); // "$"
anyamount.symbol("JPY", { locale: "ja" }); // "¥"
anyamount.symbol("USD", { locale: "en", display: "code" }); // "USD"
anyamount.symbol("USD", { locale: "en", display: "name" }); // "US dollars"
```
`anyamount.range()` formats two numbers as one range, the way the locale
writes it — shared parts collapse. Same options as the plain call.
```ts
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.parse()` goes the other way: the text a person typed, in their
locale, back to a number. The locale's separators and digits are read,
whatever wraps the number (currency, `%`, spaces) is ignored, and anything
that is not a number is `NaN` — never a throw.
```ts
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("abc", { locale: "en" }); // NaN
```
One rule beyond the locale: a separator that appears once and is followed by
one or two digits is a decimal point — `"1.5"` typed into a German form is one
and a half, not fifteen hundred; `"1.500"` keeps the locale's reading.
---
## recipes
Copy, paste, move on.
```ts
// 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 — and a table that must never abbreviate
anyamount(post.likes, { compact: true }); // "1.2K"
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
```
Output is pure — no clock reads, no environment sniffing — so server and client
render identically. Pass an explicit `locale` to keep it that way.
---
## modes
`mode` picks the rendering strategy. Each mode reads only the options that apply
to it; the rest are ignored.
| Mode | Does | Reads |
| --- | --- | --- |
| `"smart"` (default) | compact from `10000` up, plain below | `locale`, `style`, `digits` |
| `"currency"` | money; `currency` required | `locale`, `currency`, `currencyDisplay`, `digits` |
| `"unit"` | measurements; `unit` required | `locale`, `unit`, `style`, `digits` |
```ts
anyamount(1234567); // "1.2M"
anyamount(10000); // "10K"
anyamount(9999); // "9,999" — below the compact cutoff
anyamount(0.1234); // "0.12"
```
A missing `currency` or `unit` throws a `TypeError`. The options type is a
discriminated union on `mode`, so TypeScript requires them at compile time.
→ [Every mode broken down](https://anyfamily.site/docs/anyamount#modes)
---
## options
| Option | Type | Default | Used by |
| --- | --- | --- | --- |
| `mode` | `"smart" \| "currency" \| "unit"` | `"smart"` | — |
| `locale` | `string \| string[]` | runtime locale | all |
| `currency` | `string` (ISO 4217) | required | currency |
| `currencyDisplay` | `"symbol" \| "narrowSymbol" \| "code" \| "name"` | `"symbol"` | currency |
| `unit` | sanctioned unit identifier | required | unit |
| `style` | `"long" \| "short" \| "narrow"` | `"short"` | smart, unit |
| `compact` | `boolean \| number` — always / never / from this value | `10000` | smart |
| `digits` | `number` → `maximumFractionDigits` | per mode | all |
`digits` is a **ceiling, not a fixed width** — trailing zeros are never padded
on, so `digits: 2` renders `2.5`, not `2.50`. Currency mode is the exception:
the currency carries its own minimum (2 for EUR, 0 for JPY) and `Intl` keeps it.
→ [What each option does, including the digits rules](https://anyfamily.site/docs/anyamount#options)
---
## units
`Intl` supports a fixed, sanctioned list of unit identifiers plus any
`-per-` compound of them. anyamount ships the full list as a
TypeScript union, so invalid units fail at compile time.
```ts
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(3.2, { mode: "unit", unit: "gigabyte", style: "long" }); // "3.2 gigabytes"
```
→ [The full sanctioned list](https://anyfamily.site/docs/anyamount#units)
---
## locales
Any valid BCP 47 tag, including regional variants and fallback arrays. When
omitted, native `Intl` uses the runtime locale.
```ts
anyamount(1234567, { locale: "ru" }); // "1,2 млн"
anyamount(1234567, { locale: "de" }); // "1,2 Mio."
anyamount(1234567, { locale: "ja" }); // "123.5万"
anyamount(1999, { mode: "currency", currency: "USD", locale: "de" }); // "1.999,00 $"
anyamount(1999, { mode: "currency", currency: "INR", locale: "hi" }); // "₹1,999.00"
```
---
## limitations
- **No byte auto-scaling yet.** `anyamount(3200000000, { mode: "unit", unit: "byte" })`
will not pick `GB` for you — pass the unit you want.
- **No percent mode, no ranges, no parsing.** Deliberately one function, three
modes.
- **Sanctioned units only** — an `Intl` constraint, not an anyamount one.
- **Exact strings come from `Intl`** and vary between ICU versions; don't
snapshot them across environments.
---
## vs the alternatives
| | anyamount | numeral.js | accounting.js |
| --- | :---: | :---: | :---: |
| locale data bundled | **none (Intl)** | one file per locale | none, you configure it |
| locales | **200+** | registered by hand | whatever you pass |
| currency rules | **from the currency** | manual symbol | manual symbol |
| decimal digits | **per currency** | manual | manual |
| units | **sanctioned list** | no | no |
| compact notation | **every locale** | English forms | no |
| dependencies | **0** | 0 | 0 |
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.
---
## stability
anyamount follows [semver](https://semver.org/). The public API is a single
export — `anyamount`, with `anyamount.parts` and `anyamount.symbol` on it —
plus `AnyamountOptions`, `Unit` and the exported types. It only changes shape in
a major release. New options arrive in minors.
### migrating from 1.x
2.0 removed the separate `anyamountParts` and `anyamountSymbol` exports. Both
are the same functions, now reached through the one name the package exports:
```diff
- 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. Every `any*`
package follows this shape from 2.0 on: the bare call does the job, everything
else hangs off the same name.
---
## compatibility
Node.js 18+ · Chrome 77+ · Firefox 78+ · Safari 14.1+ · Edge Runtime ·
Cloudflare Workers · Deno
CI runs the full suite on Node 20, 22 and 24.
---
## the any family
anyamount is part of **any family** — tiny, zero-dependency wrappers over native
`Intl`, one API per package.
| | | |
| --- | --- | --- |
| [anywhen](https://anyfamily.site/anywhen) | dates & relative time | `Intl.DateTimeFormat` |
| [**anyamount**](https://anyfamily.site/anyamount) | numbers, currency, units | `Intl.NumberFormat` |
| [anymany](https://anyfamily.site/anymany) | lists | `Intl.ListFormat` |
| [anyaround](https://anyfamily.site/anyaround) | names & flags | `Intl.DisplayNames` |
| [anylong](https://anyfamily.site/anylong) | durations | `Intl.DurationFormat` |
| [anyplural](https://anyfamily.site/anyplural) | plurals | `Intl.PluralRules` |
| [anyword](https://anyfamily.site/anyword) | words & graphemes | `Intl.Segmenter` |
Want all of them? [`anyfamily`](https://www.npmjs.com/package/anyfamily) is one
install for the lot, and [`anyfamily-react`](https://www.npmjs.com/package/anyfamily-react)
wraps each as a hook with a shared locale provider.
```bash
npm install anyfamily
```
---
MIT © [kirilinsky](https://github.com/kirilinsky)