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
45 changes: 45 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# AGENTS.md

This file provides guidance to AI coding agents when working with code in this repository.

## Project Overview

numeric-quantity is a TypeScript library that converts human-readable numeric strings to numbers — an enhanced `parseFloat`. It handles plain numbers, fractions, mixed numbers, vulgar fraction characters, Roman numerals, and non-ASCII decimal digits from 70+ Unicode scripts.

## Commands

- **Build:** `bun run build` (tsdown → ESM, CJS, UMD)
- **Test:** `bun test` (Bun test runner; 100% coverage threshold)
- **Test single file:** `bun test src/index.test.ts`
- **Test watch:** `bun test --watch`
- **Lint:** `bun run lint` (oxlint)
- **Format:** `bun run pretty-print` (Prettier)
- **Type-check:** `tsc`
- **Docs:** `bun run docs` (TypeDoc)

## Architecture

All source lives in `src/`. The library exports three public APIs from `src/index.ts`:

- `numericQuantity()` — core parser (`src/numericQuantity.ts`). Normalizes Unicode digits, strips formatting, then matches against regex patterns for integers, decimals, fractions, mixed numbers, and scientific notation. Returns `NaN` on invalid input. Accepts an options object for Roman numeral parsing and verbose output.
- `isNumericQuantity()` — boolean wrapper (`src/isNumericQuantity.ts`)
- `parseRomanNumerals()` — standalone Roman numeral parser (`src/parseRomanNumerals.ts`)

Supporting files:
- `src/constants.ts` — regex patterns, Unicode digit range table, default options
- `src/types.ts` — TypeScript types and interfaces

## Testing

Tests use `bun:test` (import from `'bun:test'`). Test data fixtures are in `src/numericQuantityTests.ts` — add new test cases there rather than inline in the test file. Coverage must stay at 100%.

## Code Style

- 2-space indentation, single quotes, semicolons, ES5 trailing commas
- Arrow parens: avoid when possible
- Prettier handles formatting; oxlint handles linting
- Strict TypeScript (`isolatedModules`, `isolatedDeclarations` enabled)

## Build Output

Dual-package ESM + CJS with a UMD bundle. Targets ES2021 (standard) and ES2017 (legacy ESM for Webpack 4). Only `dist/` is published.
20 changes: 18 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

N/A
### Added

- [#39] `isNumericQuantity(str, options?)` function for boolean validation without parsing.
- [#39] `percentage` option to parse percentage strings (`"50%"` → `0.5` with `'decimal'`/`true`, or `50` with `'number'`).
- [#39] `allowCurrency` option to strip Unicode currency symbols (`$`, `€`, `£`, `¥`, `₹`, `₿`, etc.) from prefix or suffix.
- [#39] `verbose` option to return a detailed result object with the following fields:
- `value` — the parsed numeric value (`NaN` if invalid).
- `input` — the original input string.
- `currencyPrefix` / `currencySuffix` — currency symbol(s) stripped from start/end, if any.
- `percentageSuffix` — `true` if a `%` suffix was stripped.
- `trailingInvalid` — trailing non-numeric characters detected in the input, if any. Populated regardless of the `allowTrailingInvalid` setting.
- `sign` — the leading sign character (`'-'` or `'+'`), if present.
- `whole` — the whole-number part of a mixed fraction (e.g. `1` from `"1 2/3"`).
- `numerator` / `denominator` — fraction components (e.g. `2` and `3` from `"1 2/3"`). Always unsigned.
- [#39] Leading `+` sign support: `numericQuantity('+42')` now returns `42` instead of `NaN`. Works with all input forms including fractions (`'+1/2'`), mixed numbers (`'+1 1/2'`), decimals (`'+1.5'`), and currency (`'+$100'`).

## [v3.1.0] - 2026-02-11

### Added

- Support for non-ASCII decimal numeral systems (Arabic-Indic, Devanagari, Bengali, Thai, Fullwidth, and 70+ other Unicode `\p{Nd}` digit blocks). For example, `numericQuantity('٣')` now returns `3`.
- [#38] Support for non-ASCII decimal numeral systems (Arabic-Indic, Devanagari, Bengali, Thai, Fullwidth, and 70+ other Unicode `\p{Nd}` digit blocks). For example, `numericQuantity('٣')` now returns `3`.

## [v3.0.0] - 2026-01-21

Expand Down Expand Up @@ -190,6 +204,8 @@ N/A
[#12]: https://github.com/jakeboone02/numeric-quantity/pull/12
[#26]: https://github.com/jakeboone02/numeric-quantity/pull/26
[#37]: https://github.com/jakeboone02/numeric-quantity/pull/37
[#38]: https://github.com/jakeboone02/numeric-quantity/pull/38
[#39]: https://github.com/jakeboone02/numeric-quantity/pull/39

<!-- Release comparison links -->

Expand Down
190 changes: 173 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,16 @@
[![downloads](https://img.shields.io/npm/dm/numeric-quantity.svg)](https://npm-stat.com/charts.html?package=numeric-quantity&from=2015-08-01)
[![MIT License](https://img.shields.io/npm/l/numeric-quantity.svg)](https://opensource.org/licenses/MIT)

Converts a string to a number, like an enhanced version of `parseFloat`.
Converts a string to a number, like an enhanced version of `parseFloat`. Returns `NaN` if the provided string does not resemble a number.

**[Full documentation](https://jakeboone02.github.io/numeric-quantity/)**

Features:
In addition to plain integers and decimals, `numeric-quantity` handles:

- In addition to plain integers and decimals, `numeric-quantity` can parse numbers with comma or underscore separators (`'1,000'` or `'1_000'`), mixed numbers (`'1 2/3'`), vulgar fractions (`'1⅖'`), and the fraction slash character (`'1 2⁄3'`).
- Supports non-ASCII decimal numeral systems including Arabic-Indic (`'٣'`), Devanagari (`'३'`), Bengali (`'৩'`), Thai (`'๓'`), Fullwidth (`'3'`), and 70+ other Unicode digit scripts.
- To allow and ignore trailing invalid characters _à la_ `parseFloat`, pass `{ allowTrailingInvalid: true }` as the second argument.
- To parse Roman numerals like `'MCCXIV'` or `'Ⅻ'`, pass `{ romanNumerals: true }` as the second argument or call `parseRomanNumerals` directly.
- To parse numbers with European-style decimal comma (where `'1,0'` means `1`, not `10`), pass `{ decimalSeparator: ',' }` as the second argument.
- To produce `bigint` values when the input represents an integer that would exceeds the boundaries of `number`, pass `{ bigIntOnOverflow: true }` as the second argument.
- Results will be rounded to three decimal places by default. To avoid rounding, pass `{ round: false }` as the second argument. To round to a different number of decimal places, assign that number to the `round` option (`{ round: 5 }` will round to five decimal places).
- Returns `NaN` if the provided string does not resemble a number.
- **Fractions and mixed numbers**: `'1 2/3'` → `1.667`, `'1⅖'` → `1.4`, `'1 2⁄3'` → `1.667`
- **Separators**: `'1,000'` → `1000`, `'1_000_000'` → `1000000`
- **Roman numerals** (see [option](#roman-numerals-romannumerals) below): `'XIV'` → `14`, `'Ⅻ'` → `12`
- **Non-ASCII numerals**: Arabic-Indic (`'٣'`), Devanagari (`'३'`), Bengali, Thai, Fullwidth, and 70+ other Unicode digit scripts

> _For the inverse operation—converting a number to an imperial measurement—check out [format-quantity](https://www.npmjs.com/package/format-quantity)._

Expand Down Expand Up @@ -57,12 +53,172 @@ As UMD (all exports are properties of the global object `NumericQuantity`):

## Options

| Option | Type | Default | Description |
| ---------------------- | ----------------- | ------- | ---------------------------------------------------------------------------------------------------- |
| `round` | `number \| false` | `3` | Round the result to a certain number of decimal places. Must be greater than or equal to zero. |
| `allowTrailingInvalid` | `boolean` | `false` | Allow and ignore trailing invalid characters _à la_ `parseFloat`. |
| `romanNumerals` | `boolean` | `false` | Attempt to parse Roman numerals if Arabic numeral parsing fails. |
| `bigIntOnOverflow` | `boolean` | `false` | Generates a `bigint` value if the string represents a valid integer too large for the `number` type. |
| `decimalSeparator` | `',' \| '.'` | `"."` | Specifies which character to treat as the decimal separator. |
All options are passed as the second argument to `numericQuantity` (and `isNumericQuantity`).

### Rounding (`round`)

Results are rounded to three decimal places by default. Use the `round` option to change this behavior.

```js
numericQuantity('1/3'); // 0.333 (default: 3 decimal places)
numericQuantity('1/3', { round: 5 }); // 0.33333
numericQuantity('1/3', { round: false }); // 0.3333333333333333
```

### Trailing Invalid Characters (`allowTrailingInvalid`)

By default, strings with trailing non-numeric characters return `NaN`. Set `allowTrailingInvalid: true` to ignore trailing invalid characters, similar to `parseFloat`.

```js
numericQuantity('100abc'); // NaN
numericQuantity('100abc', { allowTrailingInvalid: true }); // 100
```

### Roman Numerals (`romanNumerals`)

Parse Roman numerals (ASCII or Unicode) by setting `romanNumerals: true`. You can also use `parseRomanNumerals` directly.

```js
numericQuantity('MCCXIV', { romanNumerals: true }); // 1214
numericQuantity('Ⅻ', { romanNumerals: true }); // 12
numericQuantity('xiv', { romanNumerals: true }); // 14 (case-insensitive)
```

### Decimal Separator (`decimalSeparator`)

For European-style numbers where comma is the decimal separator, set `decimalSeparator: ','`.

```js
numericQuantity('1,5'); // 15 (comma treated as thousands separator)
numericQuantity('1,5', { decimalSeparator: ',' }); // 1.5
numericQuantity('1.000,50', { decimalSeparator: ',' }); // 1000.5
```

### Large Integers (`bigIntOnOverflow`)

When parsing integers that exceed `Number.MAX_SAFE_INTEGER` or are less than `Number.MIN_SAFE_INTEGER`, set `bigIntOnOverflow: true` to return a `bigint` instead.

```js
numericQuantity('9007199254740992'); // 9007199254740992 (loses precision)
numericQuantity('9007199254740992', { bigIntOnOverflow: true }); // 9007199254740992n
```

### Percentages (`percentage`)

Parse percentage strings by setting the `percentage` option. Use `'decimal'` (or `true`) to divide by 100, or `'number'` to just strip the `%` symbol.

```js
numericQuantity('50%'); // NaN
numericQuantity('50%', { percentage: true }); // 0.5
numericQuantity('50%', { percentage: 'decimal' }); // 0.5
numericQuantity('50%', { percentage: 'number' }); // 50
numericQuantity('1/2%', { percentage: true }); // 0.005
```

### Currency Symbols (`allowCurrency`)

Strip currency symbols from the start or end of the string by setting `allowCurrency: true`. Supports all Unicode currency symbols (`$`, `€`, `£`, `¥`, `₹`, `₽`, `₿`, `₩`, etc.).

```js
numericQuantity('$100'); // NaN
numericQuantity('$100', { allowCurrency: true }); // 100
numericQuantity('€1.000,50', { allowCurrency: true, decimalSeparator: ',' }); // 1000.5
numericQuantity('100€', { allowCurrency: true }); // 100
numericQuantity('-$50', { allowCurrency: true }); // -50
```

### Verbose Output (`verbose`)

Set `verbose: true` to return a detailed result object instead of just the numeric value. This is useful for understanding what was parsed and stripped.

```js
numericQuantity('$50%', {
verbose: true,
allowCurrency: true,
percentage: true,
});
// {
// value: 0.5,
// input: '$50%',
// currencyPrefix: '$',
// percentageSuffix: true
// }

numericQuantity('100abc', {
verbose: true,
allowTrailingInvalid: true,
});
// {
// value: 100,
// input: '100abc',
// trailingInvalid: 'abc'
// }
```

For fraction and mixed-number inputs, the result also includes parsed fraction components (always unsigned):

```js
numericQuantity('1 2/3', { verbose: true });
// {
// value: 1.667,
// input: '1 2/3',
// whole: 1,
// numerator: 2,
// denominator: 3
// }

numericQuantity('½', { verbose: true });
// {
// value: 0.5,
// input: '½',
// numerator: 1,
// denominator: 2
// }
```

The verbose result object has the following shape:

```ts
interface NumericQuantityVerboseResult {
value: number | bigint; // The parsed value (NaN if invalid)
input: string; // Original input string
currencyPrefix?: string; // Currency symbol(s) stripped from start
currencySuffix?: string; // Currency symbol(s) stripped from end
percentageSuffix?: boolean; // True if "%" was stripped
trailingInvalid?: string; // Characters ignored (if allowTrailingInvalid)
sign?: '-' | '+'; // Leading sign character, if present
whole?: number; // Whole part of a mixed fraction (e.g. 1 from "1 2/3")
numerator?: number; // Fraction numerator (e.g. 2 from "1 2/3")
denominator?: number; // Fraction denominator (e.g. 3 from "1 2/3")
}
```

## Additional Exports

### `isNumericQuantity(str, options?): boolean`

Returns `true` if the string can be parsed as a valid number, `false` otherwise. Accepts the same options as `numericQuantity`.

```js
import { isNumericQuantity } from 'numeric-quantity';

isNumericQuantity('1 1/2'); // true
isNumericQuantity('abc'); // false
isNumericQuantity('XII', { romanNumerals: true }); // true
isNumericQuantity('$100', { allowCurrency: true }); // true
isNumericQuantity('50%', { percentage: true }); // true
```

### `parseRomanNumerals(str): number`

Parses a string of Roman numerals directly. Returns `NaN` for invalid input.

```js
import { parseRomanNumerals } from 'numeric-quantity';

parseRomanNumerals('MCMXCIX'); // 1999
parseRomanNumerals('Ⅻ'); // 12
parseRomanNumerals('invalid'); // NaN
```

[badge-npm]: https://img.shields.io/npm/v/numeric-quantity.svg?cacheSeconds=3600&logo=npm
Loading