Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ jspm_packages

# Optional REPL history
.node_repl_history

# Playground build output (copied from index.js + languages/)
site/lib/

# Netlify local state
.netlify/
10 changes: 8 additions & 2 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Server (API) - repo only, not published to npm
server/
# Archived code and the playground site - repo only, not published to npm
archive/
site/
scripts/
netlify.toml

# Tests
test/
Expand Down Expand Up @@ -39,3 +42,6 @@ Thumbs.db

# Dependencies
node_modules/

# Netlify local link state
.netlify/
38 changes: 29 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,38 @@

## Overview

`numberstring` is a JavaScript library that converts numbers to their English word representation. It supports numbers from 0 to `Number.MAX_SAFE_INTEGER` (~9 quadrillion).
`numberstring` is a zero-dependency JavaScript library that converts numbers to words in 22 languages, with ordinals, decimals, currency, fractions, Roman numerals, and BigInt support up to 10^36. A static playground in `site/` is deployed to Netlify.

## Key Functions

### Main Export: `numberstring(n, options)`
- Converts a number to English words
- Converts a number to words; forgiving: accepts integers, negatives, decimals, numeric strings, BigInt
- Returns `string` on success, `false` on invalid input
- Options: `{ cap: 'title'|'upper'|'lower', punc: '!'|'?'|'.' }`
- Options: `cap` (title, upper, lower, sentence, camel, pascal, snake, kebab, constant, dot), `punc` ('!' '?' '.'), `lang`, `point`, `and` (British), `formal` (zh/ja)

### Named Exports
- `comma(n)` - Format number with comma separators
- `group(n)` - Get magnitude group (0=ones, 1=thousands, 2=millions, etc.)

English words: `ordinal`, `nth`, `decimal`, `fraction`, `percent`, `currency`, `year`, `telephone`, `negative`, `parse`, `toWords`

Spoken and coded: `nato` (aliases `icao`, `military`), `morse`

Notation: `compact`, `scientific`, `comma`, `group`, `binary`, `octal`, `hex`, `radix`, `bytes`, `bits`

Other numeral systems (`numerals.js`): `roman`, `greek`, `egyptian`, `babylonian`, `fancy`, `clock`

Languages (cardinals, non-negative integers): `spanish`, `french`, `german`, `danish`, `chinese`, `hindi`, `russian`, `portuguese`, `japanese`, `korean`, `arabic`, `italian`, `dutch`, `turkish`, `polish`, `swedish`, `indonesian`, `thai`, `norwegian`, `finnish`, `icelandic`. `chinese` and `japanese` accept `{ formal: true }`; `spanish` and `portuguese` accept `{ cap }`.

Constants: `CAP_STYLES`, `FANCY_STYLE_NAMES`

## Usage Examples

```javascript
import numberstring, { comma, group } from 'numberstring';

numberstring(42); // 'forty-two'
numberstring(1000000); // 'one million'
numberstring(-3.14); // 'negative three point one four'
numberstring('1000000'); // 'one million'
numberstring(42, { lang: 'es' }); // 'cuarenta y dos'
numberstring(100, { cap: 'title' }); // 'One Hundred'
numberstring(50, { punc: '!' }); // 'fifty!'

Expand All @@ -31,8 +43,8 @@ group(1000000); // 2 (millions)

## Important Notes

- Only accepts non-negative integers within safe integer range
- Returns `false` for: NaN, Infinity, negative numbers, strings, objects
- Returns `false` for: NaN, Infinity, non-numeric strings, objects, values beyond 10^36
- Language modules handle non-negative integers only; negatives/decimals with a non-English `lang` return `false`
- Hyphenates compound numbers (e.g., "forty-two", "ninety-nine")
- This is an ESM-only package (use `import`, not `require`)

Expand All @@ -46,7 +58,15 @@ npm run test:coverage # Run with coverage

## Code Architecture

- `index.js` - Single file containing all logic
- `index.js` - English core and all public helpers (ordinal, nato, scientific, bytes, ...)
- `numerals.js` - roman-adjacent systems (egyptian, babylonian, greek), `fancy()` digit styles, `clock()`
- `languages/*.js` - one module per language; `test/languages.test.js` is the per-language spot-check table
- `test/extras.test.js` - tests for everything added in 1.2.0
- `index.d.ts` - hand-written types; check with `npx tsc --noEmit --strict index.d.ts`
- `site/` - playground; `scripts/build-site.js` stages the library into `site/lib/`
- `archive/` - unmaintained code (old Express server), excluded from tests and lint
- Pure functions, no side effects
- **Zero runtime dependencies.** Never add a package to `dependencies`.
- Only use Unicode blocks that macOS renders with system fonts (Mayan numerals and tally marks were dropped for this reason)
- Frozen arrays for immutable word lists
- Full JSDoc type documentation
62 changes: 62 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,68 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.2.0] - 2026-10-06

### Added

- **Casing styles** - `cap` now also accepts `sentence`, `camel`, `pascal`, `snake`, `kebab` (alias `hyphen`), `constant` (alias `screaming`), and `dot`: `numberstring(123, { cap: 'snake' })` → `one_hundred_twenty_three`. Exported as `CAP_STYLES`.
- **British `and` option** - `numberstring(123, { and: true })` → "one hundred and twenty-three", `numberstring(1001, { and: true })` → "one thousand and one". Also honored by `ordinal()`.
- **`nth(n)`** - Numeric ordinal suffix: `1st`, `22nd`, `113th`.
- **`compact(n, opt)`** - `1.5K`, `2.3B`, `1Sx`, with `digits` and `long` ("1.5 million") options.
- **`fancy(n, style)`** - Digits in Unicode styles: circled ④②, superscript ⁴², subscript, fullwidth, bold, doublestruck 𝟜𝟚, sans, monospace, keycap 4️⃣2️⃣, braille ⠼⠙⠃.
- **Alternative numeral systems** in `numerals.js`: `egyptian()` hieroglyphs (to 9,999,999), `babylonian()` base-60 cuneiform, `greek()` Ionic letters (to 9999). Mayan numerals and tally marks were tried and dropped: no system font on macOS.
- **Financial numerals** - `chinese(n, { formal: true })` → 壹仟零壹 (大写), `japanese(n, { formal: true })` → 壱千壱 (大字). Also via `toWords(n, { lang: 'zh', formal: true })`.
- **`nato(n)`** (aliases `icao`, `military`) - ICAO radiotelephony numerals: `1984` → "wun niner ait fower", `2500` → "too tousand fife hundred", `121.5` → "wun too wun decimal fife".
- **`morse(n)`** - International Morse code digits.
- `telephone(n, { oh: true })` says "oh" for zero.
- **`scientific(n)`** - Exact-mantissa scientific notation: `1984` → "1.984 × 10³", with `caret`, `e`, and `words` formats and a `digits` option.
- **`binary()`, `octal()`, `hex()`, `radix(n, base)`** - Other bases with `prefix`, `upper`, `pad` options.
- **`bytes(n)`** and **`bits(n)`** - `1.5 KB`, `1.5 KiB`, `1.5 Mb`, or "one point five kilobytes".
- **`clock(time)`** - Clock-face emoji for an hour or `H:MM`.
- `fancy()` accepts `emoji` as an alias for `keycap`, and a `clock` style that turns each digit into a clock face (814 → 🕗🕐🕓).
- `roman()` now supports vinculum notation above 3999 (a bar multiplies by 1000, two bars by a million), up to 3,999,999,999.
- Open Graph and Twitter card tags on the playground, with a PNG preview image (`site/og.png`, rendered from `site/og.svg`).
- `comma()` keeps decimals (`1,234,567.89`) and accepts numeric strings.
- Fuzz test (`test/fuzz.test.js`) proves every public function returns a value, never throws, for hostile inputs and options.
- `year()` accepts years beyond 9999 (read as cardinals) and BigInt.
- `bahasa` accepted as an alias for Indonesian.

### Fixed

- Playground: ASCII art header restored to the exact index.js block and no longer skewed by per-line centering; a malformed URL hash no longer breaks the page.
- Type declarations: per-language converters no longer advertise a `cap` option they ignore (use `toWords()` for that); Spanish and Portuguese keep it, Chinese and Japanese gain `formal`.

### Changed

- Still zero runtime dependencies.

## [1.1.0] - 2026-10-02

### Added

- **Forgiving default export** - `numberstring()` now accepts negatives, decimals, numeric strings, and a `lang` option, delegating to `negative()`, `decimal()`, and `toWords()`. Invalid input still returns `false`.
- **Playground** - Static site in `site/` deployed to Netlify. Type a number, see it in 22 languages plus ordinal, Roman, year, currency, and more.
- **Per-language spot-check tests** - `test/languages.test.js` locks in tricky numbers (21, 71, 80, 91, 100, 101, 1000, 1001, 2000, 21000, 1M, 2M, 21M) for all 22 languages.
- **TypeScript declarations** - `index.d.ts` covering the default export, every helper, options, and the language functions.
- Numbers above `Number.MAX_SAFE_INTEGER` (e.g. `1e21`) are widened to BigInt and converted instead of returning `false`.
- `npm run site` and `npm run site:build` scripts.

### Fixed

- Spanish: `100` is now `cien` (was `ciento`); `uno` apocopates before scale words (`un millón`, `veintiún mil`, `ciento un millones`).
- French: `million`/`milliard` pluralize (`deux millions`); `cents` and `quatre-vingts` drop the `s` before `mille` (`deux cent mille`).
- Italian: `-uno` elides before `mila` (`ventunmila`).
- Japanese: `一千` is kept after a higher scale word (`二万一千`, was `二万千`).
- Icelandic: gender agreement for 1–4 before `hundrað`/`þúsund` (neuter: `tvö þúsund`), `milljón` (feminine: `ein milljón`, `tvær milljónir`), and singular after numbers ending in 1 (`tuttugu og ein milljón`).
- `decimal(1e21)` returned `"one"`; exponent-form integers are now expanded, exponent-form fractions return `false`.
- `decimal()` kept only 15 digits of precision for long integer parts in strings.
- `parse('nineteen eighty-four')` returned `103`; adjacent simple words other than tens + ones now return `false`.

### Changed

- The Express REST server moved to `archive/server/` and is no longer tested, linted, or maintained. The playground supersedes it.
- Removed `express` and `supertest` from devDependencies.

## [1.0.1] - 2026-02-08

### Added
Expand Down
33 changes: 23 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ npm install # Install dev dependencies
npm test # Run Vitest tests
npm run lint # Run ESLint
npm run test:coverage # Run tests with coverage report
npm run site # Build + serve the playground at http://localhost:8080
```

## Code Style

- ES2022+ syntax (const/let, arrow functions, template literals)
- ESM modules only (`import`/`export`)
- Full JSDoc documentation
- 100% test coverage target
- Keep coverage at or above 90% (current level); 100% on functions

## Architecture

Expand All @@ -33,7 +34,7 @@ Key constants:
## When Making Changes

1. **ALWAYS run lint and tests before committing**: `npm run lint && npm test`
2. Maintain 100% coverage: `npm run test:coverage`
2. Keep coverage from dropping: `npm run test:coverage`
3. Update CHANGELOG.md for any user-facing changes
4. Preserve the fun flair (ASCII art header, tagline)

Expand All @@ -44,6 +45,15 @@ Key constants:
- Fix valid issues before pushing new commits
- Use `gh pr view <number> --comments` to fetch PR comments

## Layout

- `index.js` - core English conversion plus all public helpers; `numberstring()` is forgiving and delegates to `negative()`, `decimal()`, `toWords()`
- `numerals.js` - alternative numeral systems (egyptian, babylonian, greek) and `fancy()` Unicode digit styles; table-driven, re-exported from index.js. Only add Unicode blocks that macOS renders out of the box (Mayan numerals and tally marks did not)
- `languages/` - one module per language, cardinals only, non-negative integers only
- `test/languages.test.js` - per-language spot-check table; update expectations when fixing a language
- `site/` - static playground deployed to Netlify (`netlify.toml`); `scripts/build-site.js` copies the library into `site/lib/`. `og.png` is the social preview; after editing `og.svg` run `npm run site:og` to re-render it
- `archive/server/` - old Express API, unmaintained, excluded from tests and lint; do not extend it

## Supported Languages

- English (default)
Expand All @@ -55,17 +65,20 @@ Key constants:
- Hindi (`hi`, `hindi`, `हिन्दी`)
- Russian (`ru`, `russian`, `русский`)
- Portuguese (`pt`, `portuguese`, `português`)
- Plus Japanese, Korean, Arabic, Italian, Dutch, Turkish, Polish, Swedish, Indonesian, Thai, Norwegian, Finnish, Icelandic (22 total)

## Features

- Number to words (cardinal)
- Ordinals (1st, 2nd, 3rd)
- Decimals (3.14 → "three point one four")
- Currency ($1.23 → "one dollar and twenty-three cents")
- Fractions (1/2 → "one half")
- Roman numerals (42 → "XLII")
- Negative numbers
- BigInt support up to 10^36
Everything is exported from `index.js`; every function returns `string | false`.

- English words: `numberstring` (cardinal, forgiving input), `ordinal`, `nth`, `decimal`, `fraction`, `percent`, `currency`, `year`, `telephone` (`oh` option), `negative`, `parse` (words → number)
- 22 languages via `toWords(n, { lang })` or the named exports; `chinese`/`japanese` take `formal` for 大写/大字
- Spoken and coded: `nato` (`icao`, `military`), `morse`
- Notation: `compact` (1.5K), `scientific` (1.984 × 10³), `comma`, `binary`/`octal`/`hex`/`radix`, `bytes`/`bits`
- Other numeral systems: `roman` (vinculum above 3999), `greek`, `egyptian`, `babylonian`, `fancy` (circled, superscript, doublestruck, keycap/emoji, braille, ...), `clock`
- Options: `cap` casing styles (title, upper, lower, sentence, camel, pascal, snake, kebab, constant, dot), `punc`, `and` (British), `lang`, `point`, `formal`
- BigInt support up to 10^36; invalid input returns `false`
- **Zero runtime dependencies, always.** Never add a package to `dependencies`.

---

Expand Down
26 changes: 18 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,38 @@ We'd love help adding more languages! Here's how:

1. Create a new file in `languages/` (e.g., `pt.js` for Portuguese)
2. Follow the pattern in `languages/en.js`
3. Export a default function that converts numbers to words
4. Add your language to `languages/index.js`
5. Add tests in `test/index.test.js`
6. Update README.md with the new language
7. Submit a PR!
3. Export a default function that converts non-negative integers to words
4. Add your language and its aliases to `languages/index.js`, re-export it from `index.js`, and add it to the `toWords` switch
5. Add a row to the spot-check table in `test/languages.test.js` and a named export in `index.d.ts`
6. Update README.md (feature list, language table, direct exports) and CHANGELOG.md
7. Submit a PR against `dev`

### Pull Request Process

1. Fork the repo and create your branch from `main`
1. Fork the repo and create your branch from `dev` (PRs target `dev`; `master` is the release branch)
2. Run `npm install` to install dependencies
3. Make your changes
4. Run `npm test` to ensure tests pass
5. Run `npm run lint` to check code style
6. Update documentation if needed
7. Submit your PR!

### Adding a New Conversion

1. Add the function to `index.js` (or `numerals.js` for glyph-based systems) with JSDoc and an `@example`
2. Return `false` for input you cannot convert; never throw
3. Add it to the export block, `index.d.ts`, the README at-a-glance table and API section, and CHANGELOG.md
4. Add tests in `test/extras.test.js`
5. Add a row to the playground in `site/app.js` if it is worth seeing

### Code Style

- ES2022+ syntax (const/let, arrow functions, async/await)
- ES2022+ syntax (const/let, arrow functions, template literals)
- ESM modules only
- **Zero runtime dependencies.** Dev dependencies only.
- Add JSDoc comments for public functions
- Maintain test coverage
- Maintain test coverage (`npm run test:coverage`)
- Unicode output must render with macOS system fonts out of the box

## Code of Conduct

Expand Down
Loading
Loading