Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .oxlintrc.json

This file was deleted.

3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ All notable changes to this project will be documented in this file. Releases fo

## [1.1.0](https://github.com/mastermunj/to-numbers/compare/v1.0.0...v1.1.0) (2026-04-21)


### Features

* improve locale coverage and add CLI support ([#107](https://github.com/mastermunj/to-numbers/issues/107)) ([fdd0489](https://github.com/mastermunj/to-numbers/commit/fdd04894f69e6a5032ac7fe4e944959d433b34cf))
- improve locale coverage and add CLI support ([#107](https://github.com/mastermunj/to-numbers/issues/107)) ([fdd0489](https://github.com/mastermunj/to-numbers/commit/fdd04894f69e6a5032ac7fe4e944959d433b34cf))

## [1.0.0](https://github.com/mastermunj/to-numbers/compare/v0.0.1...v1.0.0) (2026-02-03)

Expand Down
103 changes: 82 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
[![npm downloads](https://img.shields.io/npm/dm/to-numbers.svg)](https://www.npmjs.com/package/to-numbers)
[![build](https://img.shields.io/github/actions/workflow/status/mastermunj/to-numbers/ci.yml?branch=main&label=build)](https://github.com/mastermunj/to-numbers/actions)
[![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/mastermunj/to-numbers)
[![minzipped size](https://img.shields.io/bundlephobia/minzip/to-numbers?label=minzipped)](https://bundlephobia.com/package/to-numbers)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue)](https://www.typescriptlang.org/)
[![node](https://img.shields.io/node/v/to-numbers)](https://www.npmjs.com/package/to-numbers)
[![license](https://img.shields.io/npm/l/to-numbers)](https://github.com/mastermunj/to-numbers/blob/main/LICENSE)
Expand Down Expand Up @@ -40,16 +39,17 @@ Convert words to numbers with comprehensive locale, currency, and decimal suppor
- **OCR Post-Processing** — Parse numbers recognized from scanned documents
- **Chatbots & NLP** — Understand numeric inputs in natural language
- **Accessibility** — Support users who input numbers as words
- **Localization** — Parse numbers in 124 languages and regions
- **Localization** — Parse numbers in 136 languages and regions

## ✨ Features

- **124 Locales** — The most comprehensive locale coverage available
- **136 Locales** — Locale parity with the current `to-words` package
- **High Performance** — Optimized parsing paths with benchmark coverage for real-world inputs
- **Reverse of to-words** — Perfectly complements the to-words package
- **Multiple Numbering Systems** — Short scale, Long scale, Indian, East Asian, Burmese, Khmer, and scale-first ordering
- **Currency Parsing** — Parse locale-specific currency with fractional units
- **Ordinal Numbers** — Parse ordinals across all 124 locales
- **Ordinal Numbers** — Parse ordinals across all 136 locales
- **Exact BigInt Parsing** — Preserve integers beyond `Number.MAX_SAFE_INTEGER` without precision loss
- **Structured Parse Metadata** — `parse()` returns flags such as `isCurrency`, `isNegative`, and `isOrdinal`
- **Decimal Numbers** — Handle "Point" notation and fractional units
- **Case Insensitive** — Works with any case combination
Expand Down Expand Up @@ -138,6 +138,9 @@ toNumbers.convert('One Hundred Twenty Three Point Four Five');

toNumbers.convert('Minus Fifty');
// -50

toNumbers.convert('One Quintillion');
// 1000000000000000000n
```

### Case Insensitivity
Expand Down Expand Up @@ -292,6 +295,8 @@ toNumbers('One Hundred Dollars Only', {
// 100
```

Use `{ strict: true }` for user-controlled input when unknown words should be rejected instead of ignored.

The root helper caches parser instances by locale code, so repeated calls do not recreate locale classes.
Per-locale helpers such as `to-numbers/en-US` use a single cached instance and do not require `localeCode`.

Expand Down Expand Up @@ -345,17 +350,25 @@ npx to-numbers --words "One Hundred Dollars Only" --locale en-US --currency
npx to-numbers "Forty Five Hundredths" --locale en-US
# 0.45

npx to-numbers "One Banana" --locale en-US --strict
# Error: Unknown token for locale "en-US": "banana"

npx to-numbers --detect-locale
# en-US (or the closest supported runtime locale)

npx to-numbers --help
```

Supported flags:

| Flag | Description |
| ----------------- | ---------------------------------------- |
| `--words <text>` | Provide the text to parse explicitly |
| `--locale <code>` | Choose the locale, defaulting to `en-IN` |
| `--currency` | Parse the input as a currency amount |
| `-h`, `--help` | Show help and usage examples |
| Flag | Description |
| ----------------- | ------------------------------------------ |
| `--words <text>` | Provide the text to parse explicitly |
| `--locale <code>` | Choose the locale, defaulting to `en-IN` |
| `--currency` | Parse the input as a currency amount |
| `--strict` | Reject unrecognized words |
| `--detect-locale` | Print the closest supported runtime locale |
| `-h`, `--help` | Show help and usage examples |

You can also pass the input as positional text instead of `--words`, but not both at the same time.

Expand Down Expand Up @@ -446,7 +459,7 @@ Used in: USA, UK, Canada, Australia, and most English-speaking countries.
```js
const toNumbers = new ToNumbers({ localeCode: 'en-US' });
toNumbers.convert('One Quintillion');
// 1000000000000000000
// 1000000000000000000n
```

### Long Scale (European)
Expand Down Expand Up @@ -559,6 +572,7 @@ interface ToNumbersOptions {
localeCode?: string; // Default: 'en-IN'
converterOptions?: {
currency?: boolean; // Default: false
strict?: boolean; // Default: false; reject unknown words
currencyOptions?: {
// Override locale's currency settings
name: string;
Expand All @@ -584,7 +598,7 @@ Converts words to a number.

- **text**: `string` — The text containing number words to convert
- **options**: `ConverterOptions` — Override instance options
- **returns**: `number` — The parsed numeric value
- **returns**: `number | bigint` — Safe integers and decimals are numbers; larger integers are exact bigints

```js
const toNumbers = new ToNumbers({ localeCode: 'en-US' });
Expand All @@ -611,6 +625,15 @@ toNumbers.parse('玖拾');
// { value: 90, isCurrency: false, isNegative: false }
```

For currency values beyond JavaScript's safe integer range, `currencyInfo.mainAmount` remains a `bigint` and
`currencyInfo.exactValue` provides the exact signed decimal string:

```js
const result = new ToNumbers({ localeCode: 'en-US' }).parse(hugeAmountWords, { currency: true });
result.currencyInfo.exactValue;
// '9719925474099393.01'
```

#### `toNumbers(text, options?)`

Functional equivalent of `convert()` for the full bundle.
Expand All @@ -625,6 +648,21 @@ Functional equivalent of `convert()` for the full bundle.
| ----------------- | ------- | --------- | ------------------------------------------------- |
| `currency` | boolean | false | Parse as currency with locale-specific formatting |
| `currencyOptions` | object | undefined | Override locale's default currency settings |
| `strict` | boolean | false | Reject tokens outside the selected locale grammar |

### Locale Discovery and Metadata

```js
import { detectLocale, resolveLocale } from 'to-numbers';
import { LOCALE_MANIFEST, getLocaleCapabilities } from 'to-numbers/manifest';

resolveLocale('EN_us'); // 'en-US'
detectLocale(); // Closest supported runtime locale, with en-IN fallback
getLocaleCapabilities('zh-CN').formal; // true
LOCALE_MANIFEST['en-US'].metadata.range.maximumSupported.cardinal;
```

Locale contract helpers are available from `to-numbers/locale-contract`.

## 🧪 Benchmarks

Expand All @@ -644,10 +682,10 @@ npm run bench

| Import Method | Raw | Gzip |
| ------------------------------ | --------- | -------- |
| Full bundle (all 124 locales) | 713.93 KB | 70.41 KB |
| Smallest locale bundle (si-LK) | 22.67 KB | 6.32 KB |
| Average locale bundle | — | 6.77 KB |
| Largest locale bundle (ta-IN) | 43.55 KB | 7.74 KB |
| Full bundle (all 136 locales) | 903.48 KB | 78.20 KB |
| Smallest locale bundle (uz-UZ) | 30.75 KB | 8.40 KB |
| Average locale bundle | — | 8.93 KB |
| Largest locale bundle (ta-IN) | 51.31 KB | 9.79 KB |

> **Tip:** Use tree-shakeable imports or single-locale UMD bundles for the smallest bundle size.

Expand All @@ -663,16 +701,21 @@ npm run bench

## 🗺️ Supported Locales

All 124 locales with their currencies and numbering systems:
All 136 canonical locales with their currencies and numbering systems:

| Locale | Language | Country | Currency | Scale |
| ------ | --------------- | ------------------- | ------------- | ---------- |
| af-ZA | Afrikaans | South Africa | Rand | Short |
| am-ET | Amharic | Ethiopia | ብር | Short |
| ar-AE | Arabic | UAE | درهم | Short |
| ar-DZ | Arabic | Algeria | دينار جزائري | Short |
| ar-EG | Arabic | Egypt | جنيه مصري | Short |
| ar-IQ | Arabic | Iraq | دينار عراقي | Short |
| ar-LB | Arabic | Lebanon | ليرة | Short |
| ar-MA | Arabic | Morocco | درهم | Short |
| ar-SA | Arabic | Saudi Arabia | ريال | Short |
| ar-SD | Arabic | Sudan | جنيه سوداني | Short |
| ar-YE | Arabic | Yemen | ريال يمني | Short |
| as-IN | Assamese | India | টকা | Indian |
| az-AZ | Azerbaijani | Azerbaijan | Manat | Short |
| be-BY | Belarusian | Belarus | Рубель | Short |
Expand All @@ -685,7 +728,7 @@ All 124 locales with their currencies and numbering systems:
| de-AT | German | Austria | Euro | Long |
| de-CH | German | Switzerland | Franken | Long |
| de-DE | German | Germany | Euro | Long |
| ee-EE | Estonian | Estonia | Euro | Short |
| et-EE | Estonian | Estonia | Euro | Short |
| el-GR | Greek | Greece | Ευρώ | Short |
| en-AE | English | UAE | Dirham | Short |
| en-AU | English | Australia | Dollar | Short |
Expand All @@ -696,6 +739,7 @@ All 124 locales with their currencies and numbering systems:
| en-HK | English | Hong Kong | Dollar | Short |
| en-IE | English | Ireland | Euro | Short |
| en-IN | English | India | Rupee | Indian |
| en-IQ | English | Iraq | Iraqi Dinar | Short |
| en-JM | English | Jamaica | Dollar | Short |
| en-KE | English | Kenya | Shilling | Short |
| en-LK | English | Sri Lanka | Rupee | Short |
Expand All @@ -722,6 +766,7 @@ All 124 locales with their currencies and numbering systems:
| es-CO | Spanish | Colombia | Peso | Short |
| es-ES | Spanish | Spain | Euro | Short |
| es-MX | Spanish | Mexico | Peso | Short |
| es-PE | Spanish | Peru | Sol | Short |
| es-US | Spanish | USA | Dólar | Short |
| es-VE | Spanish | Venezuela | Bolívar | Short |
| fa-IR | Persian | Iran | تومان | Short |
Expand All @@ -730,8 +775,12 @@ All 124 locales with their currencies and numbering systems:
| fr-BE | French | Belgium | Euro | Long |
| fr-CA | French | Canada | Dollar | Long |
| fr-CH | French | Switzerland | Franc | Long |
| fr-CI | French | Côte d'Ivoire | Franc CFA | Long |
| fr-CM | French | Cameroon | Franc CFA | Long |
| fr-DZ | French | Algeria | Dinar | Long |
| fr-FR | French | France | Euro | Long |
| fr-MA | French | Morocco | Dirham | Long |
| fr-MG | French | Madagascar | Ariary | Long |
| fr-SA | French | Saudi Arabia | Riyal | Long |
| gu-IN | Gujarati | India | રૂપિયો | Indian |
| ha-NG | Hausa | Nigeria | Naira | Short |
Expand All @@ -751,6 +800,7 @@ All 124 locales with their currencies and numbering systems:
| kn-IN | Kannada | India | ರೂಪಾಯಿ | Indian |
| ko-KR | Korean | South Korea | 원 | East Asian |
| lt-LT | Lithuanian | Lithuania | Euras | Short |
| lo-LA | Lao | Laos | ກີບ | Short |
| lv-LV | Latvian | Latvia | Eiro | Short |
| ml-IN | Malayalam | India | രൂപ | Indian |
| mr-IN | Marathi | India | रुपया | Indian |
Expand All @@ -760,7 +810,7 @@ All 124 locales with their currencies and numbering systems:
| nb-NO | Norwegian | Norway | Krone | Long |
| nl-NL | Dutch | Netherlands | Euro | Short |
| nl-SR | Dutch | Suriname | Dollar | Short |
| np-NP | Nepali | Nepal | रुपैयाँ | Indian |
| ne-NP | Nepali | Nepal | रुपैयाँ | Indian |
| or-IN | Odia | India | ଟଙ୍କା | Indian |
| pa-IN | Punjabi | India | ਰੁਪਇਆ | Indian |
| pl-PL | Polish | Poland | Złoty | Short |
Expand Down Expand Up @@ -801,7 +851,9 @@ All 124 locales with their currencies and numbering systems:
- **Burmese** — Burmese traditional scale (သောင်း = 10k, သိန်း = 100k, သန်း = 1M)
- **Khmer** — Khmer traditional scale (មុឺន = 10k, សែន = 100k, លាន = 1M)

**Ordinal Coverage:** all 124 locales ship ordinal data, so `convert()` and `parse()` can recognise ordinal inputs without extra flags.
Use `SUPPORTED_LOCALES` or `to-numbers/manifest` to inspect canonical locale codes and feature capabilities.

The former non-standard codes `ee-EE` and `np-NP` remain accepted as compatibility aliases for `et-EE` and `ne-NP`, but are not included in the canonical locale registry.

**Formal Numerals:** `zh-CN` and `zh-TW` support formal and financial Chinese numerals (大写 / 大寫).

Expand Down Expand Up @@ -831,6 +883,15 @@ toNumbers.convert('One');
// Error: Unknown Locale "xx-XX"
```

### Unknown Words in Strict Mode

```js
import { UnknownTokenError } from 'to-numbers';

toNumbers.convert('One Banana', { strict: true });
// UnknownTokenError: Unknown token for locale "en-IN": "banana"
```

### Handling Errors

```js
Expand Down Expand Up @@ -955,7 +1016,7 @@ See the [Contributing](#-contributing) section above. You'll need to create a lo
<details>
<summary><strong>What about ordinal numbers (First, Second, Third)?</strong></summary>

Ordinal parsing is available across all 124 locales in the current release. `convert()` and `parse()` automatically detect ordinal inputs, including suffix-, prefix-, and exact-form ordinals:
Ordinal parsing is available across all 136 locales in the current release. `convert()` and `parse()` automatically detect ordinal inputs, including suffix-, prefix-, and exact-form ordinals:

```js
const english = new ToNumbers({ localeCode: 'en-US' });
Expand Down
12 changes: 10 additions & 2 deletions __tests__/ToNumbers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ describe('ToNumbers Core Tests', () => {
test('should throw error for object input', () => {
expect(() => toNumbers.convert({} as unknown as string)).toThrow(/Invalid Input/);
});

test('supports optional strict unknown-token validation', () => {
expect(toNumbers.convert('One Banana')).toBe(1);
expect(() => toNumbers.convert('One Banana', { strict: true })).toThrow(/Unknown token/);
expect(() => toNumbers.parse('Banana One Banana', { strict: true })).toThrow(/"banana"/);
expect(() => toNumbers.parse('Banana One Pear', { strict: true })).toThrow(/Unknown tokens/);
expect(toNumbers.convert('One Hundred Rupees Only', { currency: true, strict: true })).toBe(100);
});
});

describe('Locale Tests', () => {
Expand Down Expand Up @@ -712,12 +720,12 @@ describe('Tokenizer Extended Coverage', () => {

describe('getWordVariations', () => {
test('should return variations for a word', () => {
const variations = getWordVariations('one', false);
const variations = getWordVariations('one');
expect(variations).toContain('one');
});

test('should handle case sensitive mode', () => {
const variations = getWordVariations('One', true);
const variations = getWordVariations('One');
expect(variations).toContain('One');
});
});
Expand Down
34 changes: 20 additions & 14 deletions __tests__/af-ZA.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,14 @@ describe('Test Basic Numbers from Locale Config', () => {
config.numberWordsMapping
.filter((entry) => entry && entry.value && typeof entry.number === 'number' && entry.number <= 1e15)
.forEach((entry) => {
const lowerValue = entry.value.toLowerCase();
wordToEntry.set(lowerValue, entry);
if (typeof entry.number !== 'number') {
return;
}
const number = entry.number;
const values = Array.isArray(entry.value) ? entry.value : [entry.value];
values.forEach((value) => {
wordToEntry.set(value.toLowerCase(), { number, value });
});
});

const basicTests: [string, number][] = Array.from(wordToEntry.values()).map(
Expand Down Expand Up @@ -380,18 +386,18 @@ describe('Test Ordinal Parse Result', () => {

// Fraction denominator decimal tests (Phase 3)
const testFractionDecimals: [string, number][] = [
['Nul Punt Een Tenth', 0.1],
['Nul Punt Drie Tenths', 0.3],
['Nul Punt Een Hundredth', 0.01],
['Nul Punt Drie Hundredths', 0.03],
['Nul Punt Een Thousandth', 0.001],
['Nul Punt Drie Thousandths', 0.003],
['Nul Punt Een Ten-Thousandth', 0.0001],
['Nul Punt Drie Ten-Thousandths', 0.0003],
['Nul Punt Een Hundred-Thousandth', 0.00001],
['Nul Punt Drie Hundred-Thousandths', 0.00003],
['Nul Punt Een Millionth', 0.000001],
['Nul Punt Drie Millionths', 0.000003],
['Nul Punt Een Tiende', 0.1],
['Nul Punt Drie Tiende', 0.3],
['Nul Punt Een Honderdste', 0.01],
['Nul Punt Drie Honderdste', 0.03],
['Nul Punt Een Duisendste', 0.001],
['Nul Punt Drie Duisendste', 0.003],
['Nul Punt Een Tienduisendste', 0.0001],
['Nul Punt Drie Tienduisendste', 0.0003],
['Nul Punt Een Honderdduisendste', 0.00001],
['Nul Punt Drie Honderdduisendste', 0.00003],
['Nul Punt Een Miljoenste', 0.000001],
['Nul Punt Drie Miljoenste', 0.000003],
];

describe('Test Fraction Denominator Decimals', () => {
Expand Down
Loading