Skip to content

Commit 6e8ecc9

Browse files
committed
Touch up i18n features
1 parent 1270fa2 commit 6e8ecc9

6 files changed

Lines changed: 58 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- [#39] `isNumericQuantity(str, options?)` function for boolean validation without parsing.
1313
- [#39] `percentage` option to parse percentage strings (`"50%"``0.5` with `'decimal'`/`true`, or `50` with `'number'`).
1414
- [#39] `allowCurrency` option to strip Unicode currency symbols (`$`, ``, `£`, `¥`, ``, ``, etc.) from prefix or suffix.
15-
- [#39] `verbose` option to return a detailed result object with `value`, `input`, `currencyPrefix`, `currencySuffix`, `percentageSuffix`, and `trailingInvalid` fields.
15+
- [#39] `verbose` option to return a detailed result object with `value`, `input`, `currencyPrefix`, `currencySuffix`, `percentageSuffix`, and `trailingInvalid` fields. `trailingInvalid` is populated whenever trailing non-numeric characters are detected, regardless of the `allowTrailingInvalid` setting.
1616

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

src/index.test.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { normalizeDigits } from './constants';
33
import { isNumericQuantity } from './isNumericQuantity';
44
import { numericQuantity } from './numericQuantity';
55
import { numericQuantityTests } from './numericQuantityTests';
6-
import type { NumericQuantityVerboseResult } from './types';
76

87
// Verify that decimalDigitBlockStarts covers every \p{Nd} codepoint.
98
// If a future Unicode version adds a new Nd block, this test will fail.
@@ -86,7 +85,7 @@ describe('verbose output', () => {
8685
const result = numericQuantity('$100', {
8786
verbose: true,
8887
allowCurrency: true,
89-
}) as NumericQuantityVerboseResult;
88+
});
9089
expect(result.value).toBe(100);
9190
expect(result.currencyPrefix).toBe('$');
9291
});
@@ -95,7 +94,7 @@ describe('verbose output', () => {
9594
const result = numericQuantity('100€', {
9695
verbose: true,
9796
allowCurrency: true,
98-
}) as NumericQuantityVerboseResult;
97+
});
9998
expect(result.value).toBe(100);
10099
expect(result.currencySuffix).toBe('€');
101100
});
@@ -104,7 +103,7 @@ describe('verbose output', () => {
104103
const result = numericQuantity('50%', {
105104
verbose: true,
106105
percentage: 'decimal',
107-
}) as NumericQuantityVerboseResult;
106+
});
108107
expect(result.value).toBe(0.5);
109108
expect(result.percentageSuffix).toBe(true);
110109
});
@@ -113,17 +112,26 @@ describe('verbose output', () => {
113112
const result = numericQuantity('100abc', {
114113
verbose: true,
115114
allowTrailingInvalid: true,
116-
}) as NumericQuantityVerboseResult;
115+
});
117116
expect(result.value).toBe(100);
118117
expect(result.trailingInvalid).toBe('abc');
119118
});
120119

120+
test('includes trailingInvalid even when allowTrailingInvalid is false', () => {
121+
const result = numericQuantity('100abc', {
122+
verbose: true,
123+
});
124+
expect(result.value).toBeNaN();
125+
expect(result.input).toBe('100abc');
126+
expect(result.trailingInvalid).toBe('abc');
127+
});
128+
121129
test('handles combined currency and percentage', () => {
122130
const result = numericQuantity('$50%', {
123131
verbose: true,
124132
allowCurrency: true,
125133
percentage: 'decimal',
126-
}) as NumericQuantityVerboseResult;
134+
});
127135
expect(result.value).toBe(0.5);
128136
expect(result.currencyPrefix).toBe('$');
129137
expect(result.percentageSuffix).toBe(true);
@@ -132,7 +140,7 @@ describe('verbose output', () => {
132140
test('returns NaN value for invalid input', () => {
133141
const result = numericQuantity('invalid', {
134142
verbose: true,
135-
}) as NumericQuantityVerboseResult;
143+
});
136144
expect(result.value).toBeNaN();
137145
expect(result.input).toBe('invalid');
138146
});
@@ -141,4 +149,13 @@ describe('verbose output', () => {
141149
const result = numericQuantity(42, { verbose: true });
142150
expect(result).toEqual({ value: 42, input: '42' });
143151
});
152+
153+
test('works with bigIntOnOverflow', () => {
154+
const result = numericQuantity('9007199254740992', {
155+
verbose: true,
156+
bigIntOnOverflow: true,
157+
});
158+
expect(result.value).toBe(9007199254740992n);
159+
expect(result.input).toBe('9007199254740992');
160+
});
144161
});

src/isNumericQuantity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ export const isNumericQuantity = (
1111
quantity: string | number,
1212
options?: NumericQuantityOptions
1313
): boolean => {
14-
const result = numericQuantity(quantity, options);
14+
const result = numericQuantity(quantity, { ...options, verbose: false });
1515
return typeof result === 'bigint' || !isNaN(result);
1616
};

src/numericQuantity.ts

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import {
22
defaultOptions,
33
normalizeDigits,
4-
numericRegex,
54
numericRegexWithTrailingInvalid,
65
vulgarFractionToAsciiMap,
76
vulgarFractionsRegex,
87
} from './constants';
98
import { parseRomanNumerals } from './parseRomanNumerals';
109
import type {
1110
NumericQuantityOptions,
11+
NumericQuantityReturnType,
1212
NumericQuantityVerboseResult,
1313
} from './types';
1414

@@ -23,14 +23,10 @@ const percentageSuffixRegex = /%$/;
2323
* The string can include mixed numbers, vulgar fractions, or Roman numerals.
2424
*/
2525
function numericQuantity(quantity: string | number): number;
26-
function numericQuantity(
27-
quantity: string | number,
28-
options: NumericQuantityOptions & { verbose: true }
29-
): NumericQuantityVerboseResult;
30-
function numericQuantity(
26+
function numericQuantity<T extends NumericQuantityOptions>(
3127
quantity: string | number,
32-
options: NumericQuantityOptions & { bigIntOnOverflow: true }
33-
): number | bigint;
28+
options: T
29+
): NumericQuantityReturnType<T>;
3430
function numericQuantity(
3531
quantity: string | number,
3632
options?: NumericQuantityOptions
@@ -155,27 +151,25 @@ function numericQuantity(
155151
}
156152
}
157153

158-
const regexResult = (
159-
opts.allowTrailingInvalid ? numericRegexWithTrailingInvalid : numericRegex
160-
).exec(normalizedString);
154+
const regexResult = numericRegexWithTrailingInvalid.exec(normalizedString);
161155

162156
// If the Arabic numeral regex fails, try Roman numerals
163157
if (!regexResult) {
164-
const romanResult = opts.romanNumerals
165-
? parseRomanNumerals(quantityAsString)
166-
: NaN;
167-
if (!isNaN(romanResult) && percentageSuffix) {
168-
const percentageValue =
169-
opts.percentage === 'number' ? romanResult : romanResult / 100;
170-
return returnValue(percentageValue);
171-
}
172-
return returnValue(romanResult);
158+
return returnValue(
159+
opts.romanNumerals ? parseRomanNumerals(quantityAsString) : NaN);
173160
}
174161

175-
// Capture trailing invalid characters if present (group 7 of the regex)
176-
if (opts.allowTrailingInvalid && regexResult[7]) {
177-
trailingInvalid = regexResult[7].trim();
178-
if (!trailingInvalid) trailingInvalid = undefined;
162+
// Capture trailing invalid characters: group 7 catches chars starting with
163+
// [^.\d/], but the regex (which lacks a $ anchor) may also leave unconsumed
164+
// input starting with ".", "/", or digits (e.g. "0.1.2" or "1/").
165+
const rawTrailing = (
166+
regexResult[7] || normalizedString.slice(regexResult[0].length)
167+
).trim();
168+
if (rawTrailing) {
169+
trailingInvalid = rawTrailing;
170+
if (!opts.allowTrailingInvalid) {
171+
return returnValue(NaN);
172+
}
179173
}
180174

181175
const [, dash, ng1temp, ng2temp] = regexResult;

src/numericQuantityTests.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,8 @@ export const numericQuantityTests: Record<
392392
['0.5%', 0.5, { percentage: 'number' }],
393393
['1/2%', 0.5, { percentage: 'number' }],
394394
['-50%', -50, { percentage: 'number' }],
395-
// Roman numerals with percentage
396-
['L%', 0.5, { percentage: 'decimal', romanNumerals: true }],
395+
// Roman numerals with percentage (percentage never affects Roman numeral output)
396+
['L%', 50, { percentage: 'decimal', romanNumerals: true }],
397397
['L%', 50, { percentage: 'number', romanNumerals: true }],
398398
// Without % symbol - should work normally
399399
['50', 50, { percentage: 'decimal' }],

src/types.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ export interface NumericQuantityOptions {
5555
verbose?: boolean;
5656
}
5757

58+
/**
59+
* Resolves the return type of {@link numericQuantity} based on the options provided.
60+
*/
61+
export type NumericQuantityReturnType<
62+
T extends NumericQuantityOptions | undefined = undefined,
63+
> = T extends { verbose: true }
64+
? NumericQuantityVerboseResult
65+
: T extends { bigIntOnOverflow: true }
66+
? number | bigint
67+
: number;
68+
5869
/**
5970
* Verbose result returned when `verbose: true` is set.
6071
*/
@@ -69,7 +80,7 @@ export interface NumericQuantityVerboseResult {
6980
currencySuffix?: string;
7081
/** True if a `%` suffix was stripped. */
7182
percentageSuffix?: boolean;
72-
/** Characters ignored due to `allowTrailingInvalid`, if any. */
83+
/** Trailing invalid (usually non-numeric) characters detected in the input, if any. Populated even when `allowTrailingInvalid` is `false`. */
7384
trailingInvalid?: string;
7485
}
7586

0 commit comments

Comments
 (0)