n2words 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +128 -42
- package/README.md +6 -4
- package/dist/am-ET.js +2 -2
- package/dist/am-ET.umd.js +2 -2
- package/dist/am-Latn-ET.js +2 -2
- package/dist/am-Latn-ET.umd.js +2 -2
- package/dist/ar-SA.js +2 -2
- package/dist/ar-SA.umd.js +2 -2
- package/dist/az-AZ.js +2 -2
- package/dist/az-AZ.umd.js +2 -2
- package/dist/bn-BD.js +2 -2
- package/dist/bn-BD.umd.js +2 -2
- package/dist/cs-CZ.js +2 -2
- package/dist/cs-CZ.umd.js +2 -2
- package/dist/da-DK.js +2 -2
- package/dist/da-DK.umd.js +2 -2
- package/dist/de-DE.js +2 -2
- package/dist/de-DE.umd.js +2 -2
- package/dist/el-GR.js +2 -2
- package/dist/el-GR.umd.js +2 -2
- package/dist/en-AU.js +2 -2
- package/dist/en-AU.umd.js +2 -2
- package/dist/en-BD.js +2 -2
- package/dist/en-BD.umd.js +2 -2
- package/dist/en-CA.js +2 -2
- package/dist/en-CA.umd.js +2 -2
- package/dist/en-GB.js +2 -2
- package/dist/en-GB.umd.js +2 -2
- package/dist/en-GH.js +2 -2
- package/dist/en-GH.umd.js +2 -2
- package/dist/en-IE.js +2 -2
- package/dist/en-IE.umd.js +2 -2
- package/dist/en-IN.js +2 -2
- package/dist/en-IN.umd.js +2 -2
- package/dist/en-KE.js +2 -2
- package/dist/en-KE.umd.js +2 -2
- package/dist/en-MY.js +2 -2
- package/dist/en-MY.umd.js +2 -2
- package/dist/en-NG.js +2 -2
- package/dist/en-NG.umd.js +2 -2
- package/dist/en-NZ.js +2 -2
- package/dist/en-NZ.umd.js +2 -2
- package/dist/en-PH.js +2 -2
- package/dist/en-PH.umd.js +2 -2
- package/dist/en-PK.js +2 -2
- package/dist/en-PK.umd.js +2 -2
- package/dist/en-SG.js +2 -2
- package/dist/en-SG.umd.js +2 -2
- package/dist/en-US.js +2 -2
- package/dist/en-US.umd.js +2 -2
- package/dist/en-ZA.js +2 -2
- package/dist/en-ZA.umd.js +2 -2
- package/dist/es-ES.js +2 -2
- package/dist/es-ES.umd.js +2 -2
- package/dist/es-MX.js +2 -2
- package/dist/es-MX.umd.js +2 -2
- package/dist/es-US.js +2 -2
- package/dist/es-US.umd.js +2 -2
- package/dist/fa-IR.js +2 -2
- package/dist/fa-IR.umd.js +2 -2
- package/dist/fi-FI.js +2 -2
- package/dist/fi-FI.umd.js +2 -2
- package/dist/fil-PH.js +2 -2
- package/dist/fil-PH.umd.js +2 -2
- package/dist/fr-BE.js +2 -2
- package/dist/fr-BE.umd.js +2 -2
- package/dist/fr-FR.js +2 -2
- package/dist/fr-FR.umd.js +2 -2
- package/dist/gu-IN.js +2 -2
- package/dist/gu-IN.umd.js +2 -2
- package/dist/ha-NG.js +2 -2
- package/dist/ha-NG.umd.js +2 -2
- package/dist/hbo-IL.js +2 -2
- package/dist/hbo-IL.umd.js +2 -2
- package/dist/he-IL.js +2 -2
- package/dist/he-IL.umd.js +2 -2
- package/dist/hi-IN.js +2 -2
- package/dist/hi-IN.umd.js +2 -2
- package/dist/hr-HR.js +2 -2
- package/dist/hr-HR.umd.js +2 -2
- package/dist/hu-HU.js +2 -2
- package/dist/hu-HU.umd.js +2 -2
- package/dist/id-ID.js +2 -2
- package/dist/id-ID.umd.js +2 -2
- package/dist/it-IT.js +2 -2
- package/dist/it-IT.umd.js +2 -2
- package/dist/ja-JP.js +2 -2
- package/dist/ja-JP.umd.js +2 -2
- package/dist/ka-GE.js +2 -2
- package/dist/ka-GE.umd.js +2 -2
- package/dist/kn-IN.js +2 -2
- package/dist/kn-IN.umd.js +2 -2
- package/dist/ko-KR.js +2 -2
- package/dist/ko-KR.umd.js +2 -2
- package/dist/lt-LT.js +2 -2
- package/dist/lt-LT.umd.js +2 -2
- package/dist/lv-LV.js +2 -2
- package/dist/lv-LV.umd.js +2 -2
- package/dist/mr-IN.js +2 -2
- package/dist/mr-IN.umd.js +2 -2
- package/dist/ms-MY.js +2 -2
- package/dist/ms-MY.umd.js +2 -2
- package/dist/nb-NO.js +2 -2
- package/dist/nb-NO.umd.js +2 -2
- package/dist/nl-NL.js +2 -2
- package/dist/nl-NL.umd.js +2 -2
- package/dist/pa-IN.js +2 -2
- package/dist/pa-IN.umd.js +2 -2
- package/dist/pl-PL.js +2 -2
- package/dist/pl-PL.umd.js +2 -2
- package/dist/pt-BR.js +2 -2
- package/dist/pt-BR.umd.js +2 -2
- package/dist/pt-PT.js +2 -2
- package/dist/pt-PT.umd.js +2 -2
- package/dist/ro-RO.js +2 -2
- package/dist/ro-RO.umd.js +2 -2
- package/dist/ru-RU.js +2 -2
- package/dist/ru-RU.umd.js +2 -2
- package/dist/sr-Cyrl-RS.js +2 -2
- package/dist/sr-Cyrl-RS.umd.js +2 -2
- package/dist/sr-Latn-RS.js +2 -2
- package/dist/sr-Latn-RS.umd.js +2 -2
- package/dist/sv-SE.js +2 -2
- package/dist/sv-SE.umd.js +2 -2
- package/dist/sw-KE.js +2 -2
- package/dist/sw-KE.umd.js +2 -2
- package/dist/ta-IN.js +2 -2
- package/dist/ta-IN.umd.js +2 -2
- package/dist/te-IN.js +2 -2
- package/dist/te-IN.umd.js +2 -2
- package/dist/th-TH.js +2 -2
- package/dist/th-TH.umd.js +2 -2
- package/dist/tr-TR.js +2 -2
- package/dist/tr-TR.umd.js +2 -2
- package/dist/uk-UA.js +2 -2
- package/dist/uk-UA.umd.js +2 -2
- package/dist/ur-PK.js +2 -2
- package/dist/ur-PK.umd.js +2 -2
- package/dist/vi-VN.js +2 -2
- package/dist/vi-VN.umd.js +2 -2
- package/dist/yo-NG.js +2 -2
- package/dist/yo-NG.umd.js +2 -2
- package/dist/zh-Hans-CN.js +2 -2
- package/dist/zh-Hans-CN.umd.js +2 -2
- package/dist/zh-Hant-TW.js +2 -2
- package/dist/zh-Hant-TW.umd.js +2 -2
- package/package.json +31 -22
- package/src/am-ET.d.ts +3 -5
- package/src/am-ET.js +41 -16
- package/src/am-Latn-ET.d.ts +3 -5
- package/src/am-Latn-ET.js +45 -16
- package/src/ar-SA.d.ts +44 -18
- package/src/ar-SA.js +93 -40
- package/src/az-AZ.d.ts +3 -5
- package/src/az-AZ.js +58 -20
- package/src/bn-BD.d.ts +3 -5
- package/src/bn-BD.js +32 -16
- package/src/cs-CZ.d.ts +3 -6
- package/src/cs-CZ.js +66 -42
- package/src/da-DK.d.ts +3 -6
- package/src/da-DK.js +53 -48
- package/src/de-DE.d.ts +17 -11
- package/src/de-DE.js +88 -57
- package/src/el-GR.d.ts +3 -6
- package/src/el-GR.js +45 -32
- package/src/en-AU.d.ts +17 -11
- package/src/en-AU.js +56 -41
- package/src/en-BD.d.ts +17 -11
- package/src/en-BD.js +60 -41
- package/src/en-CA.d.ts +36 -18
- package/src/en-CA.js +67 -46
- package/src/en-GB.d.ts +17 -11
- package/src/en-GB.js +56 -41
- package/src/en-GH.d.ts +32 -3
- package/src/en-GH.js +104 -26
- package/src/en-IE.d.ts +17 -11
- package/src/en-IE.js +56 -41
- package/src/en-IN.d.ts +17 -11
- package/src/en-IN.js +60 -41
- package/src/en-KE.d.ts +28 -3
- package/src/en-KE.js +93 -26
- package/src/en-MY.d.ts +26 -3
- package/src/en-MY.js +91 -26
- package/src/en-NG.d.ts +17 -11
- package/src/en-NG.js +56 -41
- package/src/en-NZ.d.ts +32 -3
- package/src/en-NZ.js +85 -31
- package/src/en-PH.d.ts +32 -3
- package/src/en-PH.js +97 -26
- package/src/en-PK.d.ts +17 -11
- package/src/en-PK.js +60 -41
- package/src/en-SG.d.ts +28 -3
- package/src/en-SG.js +93 -26
- package/src/en-US.d.ts +36 -18
- package/src/en-US.js +70 -47
- package/src/en-ZA.d.ts +17 -11
- package/src/en-ZA.js +56 -41
- package/src/es-ES.d.ts +53 -21
- package/src/es-ES.js +104 -56
- package/src/es-MX.d.ts +53 -21
- package/src/es-MX.js +104 -56
- package/src/es-US.d.ts +53 -21
- package/src/es-US.js +92 -51
- package/src/fa-IR.d.ts +3 -5
- package/src/fa-IR.js +28 -13
- package/src/fi-FI.d.ts +3 -6
- package/src/fi-FI.js +47 -29
- package/src/fil-PH.d.ts +3 -5
- package/src/fil-PH.js +61 -28
- package/src/fr-BE.d.ts +31 -15
- package/src/fr-BE.js +128 -57
- package/src/fr-FR.d.ts +31 -16
- package/src/fr-FR.js +97 -60
- package/src/gu-IN.d.ts +3 -5
- package/src/gu-IN.js +31 -16
- package/src/ha-NG.d.ts +3 -5
- package/src/ha-NG.js +55 -27
- package/src/hbo-IL.d.ts +26 -12
- package/src/hbo-IL.js +92 -51
- package/src/he-IL.d.ts +17 -10
- package/src/he-IL.js +92 -50
- package/src/hi-IN.d.ts +3 -5
- package/src/hi-IN.js +30 -17
- package/src/hr-HR.d.ts +21 -10
- package/src/hr-HR.js +89 -33
- package/src/hu-HU.d.ts +3 -5
- package/src/hu-HU.js +57 -23
- package/src/id-ID.d.ts +3 -5
- package/src/id-ID.js +56 -23
- package/src/it-IT.d.ts +17 -11
- package/src/it-IT.js +74 -43
- package/src/ja-JP.d.ts +3 -6
- package/src/ja-JP.js +39 -26
- package/src/ka-GE.d.ts +3 -6
- package/src/ka-GE.js +38 -26
- package/src/kn-IN.d.ts +3 -5
- package/src/kn-IN.js +31 -16
- package/src/ko-KR.d.ts +3 -6
- package/src/ko-KR.js +34 -26
- package/src/lt-LT.d.ts +21 -11
- package/src/lt-LT.js +64 -42
- package/src/lv-LV.d.ts +21 -11
- package/src/lv-LV.js +79 -51
- package/src/mr-IN.d.ts +3 -5
- package/src/mr-IN.js +31 -16
- package/src/ms-MY.d.ts +3 -5
- package/src/ms-MY.js +58 -24
- package/src/nb-NO.d.ts +3 -6
- package/src/nb-NO.js +54 -34
- package/src/nl-NL.d.ts +41 -20
- package/src/nl-NL.js +111 -69
- package/src/pa-IN.d.ts +3 -5
- package/src/pa-IN.js +32 -16
- package/src/pl-PL.d.ts +21 -11
- package/src/pl-PL.js +69 -45
- package/src/pt-BR.d.ts +22 -11
- package/src/pt-BR.js +93 -53
- package/src/pt-PT.d.ts +17 -11
- package/src/pt-PT.js +80 -48
- package/src/ro-RO.d.ts +21 -11
- package/src/ro-RO.js +77 -39
- package/src/ru-RU.d.ts +35 -15
- package/src/ru-RU.js +100 -38
- package/src/sr-Cyrl-RS.d.ts +35 -15
- package/src/sr-Cyrl-RS.js +100 -38
- package/src/sr-Latn-RS.d.ts +35 -15
- package/src/sr-Latn-RS.js +100 -38
- package/src/sv-SE.d.ts +3 -6
- package/src/sv-SE.js +53 -34
- package/src/sw-KE.d.ts +3 -5
- package/src/sw-KE.js +50 -20
- package/src/ta-IN.d.ts +3 -5
- package/src/ta-IN.js +29 -17
- package/src/te-IN.d.ts +3 -5
- package/src/te-IN.js +31 -16
- package/src/th-TH.d.ts +3 -5
- package/src/th-TH.js +42 -19
- package/src/tr-TR.d.ts +17 -11
- package/src/tr-TR.js +63 -37
- package/src/uk-UA.d.ts +21 -10
- package/src/uk-UA.js +89 -33
- package/src/ur-PK.d.ts +3 -5
- package/src/ur-PK.js +32 -16
- package/src/utils/check-max.d.ts +26 -0
- package/src/utils/check-max.js +33 -0
- package/src/utils/expand-scientific.d.ts +0 -4
- package/src/utils/expand-scientific.js +7 -9
- package/src/utils/is-plain-object.d.ts +3 -4
- package/src/utils/is-plain-object.js +3 -4
- package/src/utils/parse-cardinal.d.ts +1 -2
- package/src/utils/parse-cardinal.js +12 -9
- package/src/utils/parse-currency.d.ts +1 -2
- package/src/utils/parse-currency.js +9 -11
- package/src/utils/parse-ordinal.d.ts +0 -1
- package/src/utils/parse-ordinal.js +9 -10
- package/src/utils/resolve-options.d.ts +17 -0
- package/src/utils/resolve-options.js +56 -0
- package/src/utils/scale.d.ts +49 -0
- package/src/utils/scale.js +65 -0
- package/src/vi-VN.d.ts +3 -6
- package/src/vi-VN.js +41 -28
- package/src/yo-NG.d.ts +3 -5
- package/src/yo-NG.js +49 -33
- package/src/zh-Hans-CN.d.ts +45 -20
- package/src/zh-Hans-CN.js +84 -31
- package/src/zh-Hant-TW.d.ts +45 -20
- package/src/zh-Hant-TW.js +85 -34
- package/src/utils/validate-options.d.ts +0 -8
- package/src/utils/validate-options.js +0 -16
|
@@ -10,24 +10,23 @@ import { expandScientificNotation, hasScientificNotation, numberToString } from
|
|
|
10
10
|
/**
|
|
11
11
|
* Parses a value for cardinal conversion.
|
|
12
12
|
* Cardinals accept any numeric value: integers, decimals, negatives.
|
|
13
|
-
*
|
|
14
13
|
* @param {number|string|bigint} value - The value to parse
|
|
15
|
-
* @returns {{isNegative: boolean, integerPart: bigint, decimalPart?: string}}
|
|
14
|
+
* @returns {{isNegative: boolean, integerPart: bigint, decimalPart?: string}} The parsed cardinal components
|
|
16
15
|
* @throws {TypeError} If value is not number, string, or bigint
|
|
17
16
|
* @throws {RangeError} If value is not finite
|
|
18
17
|
*/
|
|
19
|
-
export function parseCardinalValue
|
|
18
|
+
export function parseCardinalValue(value) {
|
|
20
19
|
const type = typeof value
|
|
21
20
|
|
|
22
21
|
// BigInt: simplest case
|
|
23
|
-
if (
|
|
22
|
+
if (typeof value === 'bigint') {
|
|
24
23
|
return value < 0n
|
|
25
24
|
? { isNegative: true, integerPart: -value }
|
|
26
25
|
: { isNegative: false, integerPart: value }
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
// Number: fast path for safe integers
|
|
30
|
-
if (
|
|
29
|
+
if (typeof value === 'number') {
|
|
31
30
|
if (!Number.isFinite(value)) {
|
|
32
31
|
throw new RangeError('Number must be finite (NaN and Infinity are not supported)')
|
|
33
32
|
}
|
|
@@ -40,19 +39,21 @@ export function parseCardinalValue (value) {
|
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
// String input
|
|
43
|
-
if (
|
|
42
|
+
if (typeof value === 'string') {
|
|
44
43
|
return parseNumericString(normalizeString(value))
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
throw new TypeError(
|
|
48
|
-
`Invalid value type: expected number, string, or bigint, received ${type}
|
|
47
|
+
`Invalid value type: expected number, string, or bigint, received ${type}`,
|
|
49
48
|
)
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
/**
|
|
53
52
|
* Validates and normalizes a string numeric input.
|
|
53
|
+
* @param {string} value - The string to normalize
|
|
54
|
+
* @returns {string} The normalized numeric string
|
|
54
55
|
*/
|
|
55
|
-
function normalizeString
|
|
56
|
+
function normalizeString(value) {
|
|
56
57
|
const trimmed = value.trim()
|
|
57
58
|
if (trimmed.length === 0 || Number.isNaN(Number(trimmed))) {
|
|
58
59
|
throw new RangeError(`Invalid number format: "${value}"`)
|
|
@@ -62,8 +63,10 @@ function normalizeString (value) {
|
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
65
|
* Parses a normalized numeric string into components.
|
|
66
|
+
* @param {string} str - The normalized numeric string
|
|
67
|
+
* @returns {{isNegative: boolean, integerPart: bigint, decimalPart?: string}} The parsed numeric components
|
|
65
68
|
*/
|
|
66
|
-
function parseNumericString
|
|
69
|
+
function parseNumericString(str) {
|
|
67
70
|
const isNegative = str[0] === '-'
|
|
68
71
|
if (isNegative) str = str.slice(1)
|
|
69
72
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parses a value for currency conversion.
|
|
3
3
|
* Returns dollars and cents as separate bigints, plus negative flag.
|
|
4
|
-
*
|
|
5
4
|
* @param {number|string|bigint} value - The value to parse
|
|
6
|
-
* @returns {{isNegative: boolean, dollars: bigint, cents: bigint}}
|
|
5
|
+
* @returns {{isNegative: boolean, dollars: bigint, cents: bigint}} The parsed dollars and cents with a negative flag.
|
|
7
6
|
* @throws {TypeError} If value is not number, string, or bigint
|
|
8
7
|
* @throws {RangeError} If value is not finite
|
|
9
8
|
*/
|
|
@@ -9,24 +9,23 @@ import { expandScientificNotation, hasScientificNotation, numberToString } from
|
|
|
9
9
|
/**
|
|
10
10
|
* Parses a value for currency conversion.
|
|
11
11
|
* Returns dollars and cents as separate bigints, plus negative flag.
|
|
12
|
-
*
|
|
13
12
|
* @param {number|string|bigint} value - The value to parse
|
|
14
|
-
* @returns {{isNegative: boolean, dollars: bigint, cents: bigint}}
|
|
13
|
+
* @returns {{isNegative: boolean, dollars: bigint, cents: bigint}} The parsed dollars and cents with a negative flag.
|
|
15
14
|
* @throws {TypeError} If value is not number, string, or bigint
|
|
16
15
|
* @throws {RangeError} If value is not finite
|
|
17
16
|
*/
|
|
18
|
-
export function parseCurrencyValue
|
|
17
|
+
export function parseCurrencyValue(value) {
|
|
19
18
|
const type = typeof value
|
|
20
19
|
|
|
21
20
|
// BigInt: whole dollars only
|
|
22
|
-
if (
|
|
21
|
+
if (typeof value === 'bigint') {
|
|
23
22
|
return value < 0n
|
|
24
23
|
? { isNegative: true, dollars: -value, cents: 0n }
|
|
25
24
|
: { isNegative: false, dollars: value, cents: 0n }
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
// Number: fast path for safe integers
|
|
29
|
-
if (
|
|
28
|
+
if (typeof value === 'number') {
|
|
30
29
|
if (!Number.isFinite(value)) {
|
|
31
30
|
throw new RangeError('Currency must be a finite number')
|
|
32
31
|
}
|
|
@@ -40,22 +39,21 @@ export function parseCurrencyValue (value) {
|
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
// String input
|
|
43
|
-
if (
|
|
42
|
+
if (typeof value === 'string') {
|
|
44
43
|
return parseCurrencyString(value)
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
throw new TypeError(
|
|
48
|
-
`Invalid value type: expected number, string, or bigint, received ${type}
|
|
47
|
+
`Invalid value type: expected number, string, or bigint, received ${type}`,
|
|
49
48
|
)
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
/**
|
|
53
52
|
* Parses a string for currency conversion.
|
|
54
|
-
*
|
|
55
53
|
* @param {string} value - The string to parse
|
|
56
|
-
* @returns {{isNegative: boolean, dollars: bigint, cents: bigint}}
|
|
54
|
+
* @returns {{isNegative: boolean, dollars: bigint, cents: bigint}} The parsed dollars and cents with a negative flag.
|
|
57
55
|
*/
|
|
58
|
-
function parseCurrencyString
|
|
56
|
+
function parseCurrencyString(value) {
|
|
59
57
|
let str = value.trim()
|
|
60
58
|
|
|
61
59
|
if (str.length === 0 || Number.isNaN(Number(str))) {
|
|
@@ -86,6 +84,6 @@ function parseCurrencyString (value) {
|
|
|
86
84
|
return {
|
|
87
85
|
isNegative,
|
|
88
86
|
dollars: BigInt(dollarStr),
|
|
89
|
-
cents: BigInt(centStr)
|
|
87
|
+
cents: BigInt(centStr),
|
|
90
88
|
}
|
|
91
89
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parses a value for ordinal conversion.
|
|
3
3
|
* Ordinals require positive integers only (no zero, negatives, or decimals).
|
|
4
|
-
*
|
|
5
4
|
* @param {number|string|bigint} value - The value to parse
|
|
6
5
|
* @returns {bigint} The positive integer value
|
|
7
6
|
* @throws {TypeError} If value is not number, string, or bigint
|
|
@@ -9,17 +9,16 @@ import { expandScientificNotation, hasScientificNotation } from './expand-scient
|
|
|
9
9
|
/**
|
|
10
10
|
* Parses a value for ordinal conversion.
|
|
11
11
|
* Ordinals require positive integers only (no zero, negatives, or decimals).
|
|
12
|
-
*
|
|
13
12
|
* @param {number|string|bigint} value - The value to parse
|
|
14
13
|
* @returns {bigint} The positive integer value
|
|
15
14
|
* @throws {TypeError} If value is not number, string, or bigint
|
|
16
15
|
* @throws {RangeError} If value is zero, negative, or has a decimal part
|
|
17
16
|
*/
|
|
18
|
-
export function parseOrdinalValue
|
|
17
|
+
export function parseOrdinalValue(value) {
|
|
19
18
|
const type = typeof value
|
|
20
19
|
|
|
21
20
|
// BigInt: simplest case
|
|
22
|
-
if (
|
|
21
|
+
if (typeof value === 'bigint') {
|
|
23
22
|
if (value <= 0n) {
|
|
24
23
|
throw new RangeError('Ordinals must be positive integers')
|
|
25
24
|
}
|
|
@@ -27,7 +26,7 @@ export function parseOrdinalValue (value) {
|
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
// Number: fast path for safe integers
|
|
30
|
-
if (
|
|
29
|
+
if (typeof value === 'number') {
|
|
31
30
|
if (!Number.isFinite(value)) {
|
|
32
31
|
throw new RangeError('Ordinals must be finite numbers')
|
|
33
32
|
}
|
|
@@ -41,23 +40,22 @@ export function parseOrdinalValue (value) {
|
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
// String input
|
|
44
|
-
if (
|
|
43
|
+
if (typeof value === 'string') {
|
|
45
44
|
return parseOrdinalString(value)
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
throw new TypeError(
|
|
49
|
-
`Invalid value type: expected number, string, or bigint, received ${type}
|
|
48
|
+
`Invalid value type: expected number, string, or bigint, received ${type}`,
|
|
50
49
|
)
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
/**
|
|
54
53
|
* Parses a string for ordinal conversion.
|
|
55
|
-
*
|
|
56
54
|
* @param {string} value - The string to parse
|
|
57
55
|
* @returns {bigint} The positive integer value
|
|
58
56
|
* @throws {RangeError} If string is not a valid positive integer
|
|
59
57
|
*/
|
|
60
|
-
function parseOrdinalString
|
|
58
|
+
function parseOrdinalString(value) {
|
|
61
59
|
const trimmed = value.trim()
|
|
62
60
|
|
|
63
61
|
if (trimmed.length === 0) {
|
|
@@ -96,8 +94,9 @@ function parseOrdinalString (value) {
|
|
|
96
94
|
throw new RangeError('Ordinals must be positive integers')
|
|
97
95
|
}
|
|
98
96
|
return result
|
|
99
|
-
}
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
100
99
|
if (e instanceof RangeError) throw e
|
|
101
|
-
throw new RangeError(`Invalid ordinal format: "${value}"
|
|
100
|
+
throw new RangeError(`Invalid ordinal format: "${value}"`, { cause: e })
|
|
102
101
|
}
|
|
103
102
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply a form's option defaults and reject anything it doesn't declare.
|
|
3
|
+
*
|
|
4
|
+
* The exported defaults map (e.g. `cardinalDefaults`) is the single source of
|
|
5
|
+
* truth for each option's default value — the form never restates it, and the
|
|
6
|
+
* docs generator imports it rather than scraping the body. A malformed options
|
|
7
|
+
* argument (unknown key, wrong-typed value, inherited key) throws `TypeError`;
|
|
8
|
+
* a known option with a value outside its declared allowed set (an exported
|
|
9
|
+
* `<form>Values` map, e.g. gender) throws `RangeError` — right kind of value,
|
|
10
|
+
* out of range — instead of silently falling back to the default.
|
|
11
|
+
* @template {object} T
|
|
12
|
+
* @param {T | undefined} options - Caller-provided options, or undefined
|
|
13
|
+
* @param {Required<T>} defaults - The form's default for every option
|
|
14
|
+
* @param {Partial<Record<keyof T & string, readonly unknown[]>>} [values] - Allowed set per enum-valued option
|
|
15
|
+
* @returns {Required<T>} The options with every default applied
|
|
16
|
+
*/
|
|
17
|
+
export function resolveOptions<T extends object>(options: T | undefined, defaults: Required<T>, values?: Partial<Record<keyof T & string, readonly unknown[]>>): Required<T>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { isPlainObject } from './is-plain-object.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Apply a form's option defaults and reject anything it doesn't declare.
|
|
5
|
+
*
|
|
6
|
+
* The exported defaults map (e.g. `cardinalDefaults`) is the single source of
|
|
7
|
+
* truth for each option's default value — the form never restates it, and the
|
|
8
|
+
* docs generator imports it rather than scraping the body. A malformed options
|
|
9
|
+
* argument (unknown key, wrong-typed value, inherited key) throws `TypeError`;
|
|
10
|
+
* a known option with a value outside its declared allowed set (an exported
|
|
11
|
+
* `<form>Values` map, e.g. gender) throws `RangeError` — right kind of value,
|
|
12
|
+
* out of range — instead of silently falling back to the default.
|
|
13
|
+
* @template {object} T
|
|
14
|
+
* @param {T | undefined} options - Caller-provided options, or undefined
|
|
15
|
+
* @param {Required<T>} defaults - The form's default for every option
|
|
16
|
+
* @param {Partial<Record<keyof T & string, readonly unknown[]>>} [values] - Allowed set per enum-valued option
|
|
17
|
+
* @returns {Required<T>} The options with every default applied
|
|
18
|
+
*/
|
|
19
|
+
export function resolveOptions(options, defaults, values) {
|
|
20
|
+
/** @type {Record<string, unknown>} */
|
|
21
|
+
const resolved = { ...defaults }
|
|
22
|
+
if (options === undefined) return /** @type {Required<T>} */ (resolved)
|
|
23
|
+
|
|
24
|
+
if (!isPlainObject(options)) {
|
|
25
|
+
throw new TypeError(`Invalid options: expected plain object or undefined, got ${typeof options}`)
|
|
26
|
+
}
|
|
27
|
+
const allowed = Object.keys(defaults)
|
|
28
|
+
for (const [key, value] of Object.entries(options)) {
|
|
29
|
+
// Own-property check: inherited keys (__proto__, constructor, …) are not
|
|
30
|
+
// options. Using `key in defaults` here would let them past the guard and
|
|
31
|
+
// into `resolved[key] = value` — a prototype-pollution vector. An unknown
|
|
32
|
+
// key is a malformed-argument (shape) error, hence TypeError — RangeError is
|
|
33
|
+
// reserved for a value outside an allowed range/set (see checkMax).
|
|
34
|
+
if (!Object.hasOwn(defaults, key)) {
|
|
35
|
+
throw new TypeError(`Unknown option "${key}" — expected one of: ${allowed.join(', ')}`)
|
|
36
|
+
}
|
|
37
|
+
// `{ key: undefined }` means "use the default": `key?: T` is `T | undefined`
|
|
38
|
+
// without exactOptionalPropertyTypes, and the old destructuring defaults
|
|
39
|
+
// treated undefined the same way. Omit it rather than reject it as a type.
|
|
40
|
+
if (value === undefined) continue
|
|
41
|
+
if (typeof value !== typeof resolved[key]) {
|
|
42
|
+
throw new TypeError(`Option "${key}" must be a ${typeof resolved[key]}, got ${typeof value}`)
|
|
43
|
+
}
|
|
44
|
+
if (values !== undefined && Object.hasOwn(values, key)) {
|
|
45
|
+
const set = /** @type {Record<string, readonly unknown[]>} */ (values)[key]
|
|
46
|
+
if (!set.includes(value)) {
|
|
47
|
+
// The received value is caller-supplied and arbitrary — stringify it so
|
|
48
|
+
// quotes/newlines can't garble the message. The allowed set is our own
|
|
49
|
+
// gate-verified declaration and reads cleaner unquoted.
|
|
50
|
+
throw new RangeError(`Option "${key}" must be one of: ${set.join(', ')} — got ${JSON.stringify(value)}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
resolved[key] = value
|
|
54
|
+
}
|
|
55
|
+
return /** @type {Required<T>} */ (resolved)
|
|
56
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 3-digit grouping, scale array starting at "thousand" (en-US, de-DE, ru-RU, …).
|
|
3
|
+
* @param {number} scaleCount Number of scale words in the table
|
|
4
|
+
* @returns {bigint} The first unsupported value (10^((scaleCount + 1) * 3))
|
|
5
|
+
*/
|
|
6
|
+
export function western(scaleCount: number): bigint;
|
|
7
|
+
/**
|
|
8
|
+
* Myriad (4-digit) grouping — one scale word per power of 10,000 (ja-JP, ko-KR).
|
|
9
|
+
* @param {number} scaleCount Number of scale words in the table
|
|
10
|
+
* @returns {bigint} The first unsupported value
|
|
11
|
+
*/
|
|
12
|
+
export function myriad(scaleCount: number): bigint;
|
|
13
|
+
/**
|
|
14
|
+
* South Asian 3-2-2 grouping: a 3-digit base segment then 2 digits per scale
|
|
15
|
+
* word, with the table's index 0 reserved for the (empty) units slot.
|
|
16
|
+
* @param {number} wordCount Length of the scale-word table (including the units slot)
|
|
17
|
+
* @returns {bigint} The first unsupported value
|
|
18
|
+
*/
|
|
19
|
+
export function indian(wordCount: number): bigint;
|
|
20
|
+
/**
|
|
21
|
+
* Long scale — each unit (a base scale word, or a prefix) yields two levels
|
|
22
|
+
* (-illion / -illiard), spanning 6 powers of ten. es-*, fr-*, it-IT.
|
|
23
|
+
* @param {number} unitCount Number of base scale words or prefixes
|
|
24
|
+
* @returns {bigint} The first unsupported value (10^(6 * (unitCount + 1)))
|
|
25
|
+
*/
|
|
26
|
+
export function longScale(unitCount: number): bigint;
|
|
27
|
+
/**
|
|
28
|
+
* Escape hatch: a structural bound given as its base-10 exponent (zh: `bounded(16)`).
|
|
29
|
+
* @param {number} exponent The base-10 exponent of the ceiling
|
|
30
|
+
* @returns {bigint} 10^exponent
|
|
31
|
+
*/
|
|
32
|
+
export function bounded(exponent: number): bigint;
|
|
33
|
+
/**
|
|
34
|
+
* Scale-range helpers — pure.
|
|
35
|
+
*
|
|
36
|
+
* Each form declares its maximum supported value as a bigint export
|
|
37
|
+
* (`cardinalMax`, `ordinalMax`, `currencyMax`): the smallest value the form
|
|
38
|
+
* refuses, so the largest it converts is that minus one. `UNBOUNDED` (null)
|
|
39
|
+
* means no fixed limit.
|
|
40
|
+
*
|
|
41
|
+
* These helpers derive that bigint from a language's own scale-table size, so
|
|
42
|
+
* the ceiling tracks the vocabulary and can't drift. A language whose shape
|
|
43
|
+
* fits none of them declares the value directly (`bounded(n)` or a literal
|
|
44
|
+
* bigint). They only *produce* a max — the range check that consumes one
|
|
45
|
+
* (`checkMax`) lives in check-max.js, and the verification checks (boundary,
|
|
46
|
+
* gaps, injectivity) live in the gate. Nothing here throws.
|
|
47
|
+
*/
|
|
48
|
+
/** No fixed ceiling — recursive/compounding spellers (th-TH, fa-IR, …). */
|
|
49
|
+
export const UNBOUNDED: null;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scale-range helpers — pure.
|
|
3
|
+
*
|
|
4
|
+
* Each form declares its maximum supported value as a bigint export
|
|
5
|
+
* (`cardinalMax`, `ordinalMax`, `currencyMax`): the smallest value the form
|
|
6
|
+
* refuses, so the largest it converts is that minus one. `UNBOUNDED` (null)
|
|
7
|
+
* means no fixed limit.
|
|
8
|
+
*
|
|
9
|
+
* These helpers derive that bigint from a language's own scale-table size, so
|
|
10
|
+
* the ceiling tracks the vocabulary and can't drift. A language whose shape
|
|
11
|
+
* fits none of them declares the value directly (`bounded(n)` or a literal
|
|
12
|
+
* bigint). They only *produce* a max — the range check that consumes one
|
|
13
|
+
* (`checkMax`) lives in check-max.js, and the verification checks (boundary,
|
|
14
|
+
* gaps, injectivity) live in the gate. Nothing here throws.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** No fixed ceiling — recursive/compounding spellers (th-TH, fa-IR, …). */
|
|
18
|
+
export const UNBOUNDED = null
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 3-digit grouping, scale array starting at "thousand" (en-US, de-DE, ru-RU, …).
|
|
22
|
+
* @param {number} scaleCount Number of scale words in the table
|
|
23
|
+
* @returns {bigint} The first unsupported value (10^((scaleCount + 1) * 3))
|
|
24
|
+
*/
|
|
25
|
+
export function western(scaleCount) {
|
|
26
|
+
return 10n ** BigInt((scaleCount + 1) * 3)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Myriad (4-digit) grouping — one scale word per power of 10,000 (ja-JP, ko-KR).
|
|
31
|
+
* @param {number} scaleCount Number of scale words in the table
|
|
32
|
+
* @returns {bigint} The first unsupported value
|
|
33
|
+
*/
|
|
34
|
+
export function myriad(scaleCount) {
|
|
35
|
+
return 10n ** BigInt((scaleCount + 1) * 4)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* South Asian 3-2-2 grouping: a 3-digit base segment then 2 digits per scale
|
|
40
|
+
* word, with the table's index 0 reserved for the (empty) units slot.
|
|
41
|
+
* @param {number} wordCount Length of the scale-word table (including the units slot)
|
|
42
|
+
* @returns {bigint} The first unsupported value
|
|
43
|
+
*/
|
|
44
|
+
export function indian(wordCount) {
|
|
45
|
+
return 10n ** BigInt(3 + 2 * (wordCount - 1))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Long scale — each unit (a base scale word, or a prefix) yields two levels
|
|
50
|
+
* (-illion / -illiard), spanning 6 powers of ten. es-*, fr-*, it-IT.
|
|
51
|
+
* @param {number} unitCount Number of base scale words or prefixes
|
|
52
|
+
* @returns {bigint} The first unsupported value (10^(6 * (unitCount + 1)))
|
|
53
|
+
*/
|
|
54
|
+
export function longScale(unitCount) {
|
|
55
|
+
return 10n ** BigInt(6 * (unitCount + 1))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Escape hatch: a structural bound given as its base-10 exponent (zh: `bounded(16)`).
|
|
60
|
+
* @param {number} exponent The base-10 exponent of the ceiling
|
|
61
|
+
* @returns {bigint} 10^exponent
|
|
62
|
+
*/
|
|
63
|
+
export function bounded(exponent) {
|
|
64
|
+
return 10n ** BigInt(exponent)
|
|
65
|
+
}
|
package/src/vi-VN.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
export const cardinalMax: bigint;
|
|
2
|
+
export const ordinalMax: bigint;
|
|
3
|
+
export const currencyMax: bigint;
|
|
1
4
|
/**
|
|
2
5
|
* Converts a numeric value to Vietnamese words.
|
|
3
6
|
*
|
|
4
7
|
* This is the main public API. It accepts any valid numeric input
|
|
5
8
|
* (number, string, or bigint) and handles parsing internally.
|
|
6
|
-
*
|
|
7
9
|
* @param {number | string | bigint} value - The numeric value to convert
|
|
8
10
|
* @returns {string} The number in Vietnamese words
|
|
9
11
|
* @throws {TypeError} If value is not a valid numeric type
|
|
10
12
|
* @throws {Error} If value is not a valid number format
|
|
11
|
-
*
|
|
12
13
|
* @example
|
|
13
14
|
* toCardinal(42) // 'bốn mươi hai'
|
|
14
15
|
* toCardinal(101) // 'một trăm lẻ một'
|
|
@@ -17,12 +18,10 @@
|
|
|
17
18
|
export function toCardinal(value: number | string | bigint): string;
|
|
18
19
|
/**
|
|
19
20
|
* Converts a numeric value to Vietnamese ordinal words.
|
|
20
|
-
*
|
|
21
21
|
* @param {number | string | bigint} value - The numeric value to convert (positive integer)
|
|
22
22
|
* @returns {string} The number as ordinal words
|
|
23
23
|
* @throws {TypeError} If value is not a valid numeric type
|
|
24
24
|
* @throws {RangeError} If value is negative, zero, or has a decimal part
|
|
25
|
-
*
|
|
26
25
|
* @example
|
|
27
26
|
* toOrdinal(1) // 'thứ nhất'
|
|
28
27
|
* toOrdinal(2) // 'thứ hai'
|
|
@@ -34,12 +33,10 @@ export function toOrdinal(value: number | string | bigint): string;
|
|
|
34
33
|
*
|
|
35
34
|
* Vietnamese Dong has no subunit in modern usage (xu are historical).
|
|
36
35
|
* Amounts are rounded to whole đồng.
|
|
37
|
-
*
|
|
38
36
|
* @param {number | string | bigint} value - The currency amount to convert
|
|
39
37
|
* @returns {string} The amount in Vietnamese currency words
|
|
40
38
|
* @throws {TypeError} If value is not a valid numeric type
|
|
41
39
|
* @throws {Error} If value is not a valid number format
|
|
42
|
-
*
|
|
43
40
|
* @example
|
|
44
41
|
* toCurrency(42) // 'bốn mươi hai đồng'
|
|
45
42
|
* toCurrency(1000) // 'một nghìn đồng'
|
package/src/vi-VN.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import { parseCardinalValue } from './utils/parse-cardinal.js'
|
|
13
13
|
import { parseCurrencyValue } from './utils/parse-currency.js'
|
|
14
14
|
import { parseOrdinalValue } from './utils/parse-ordinal.js'
|
|
15
|
+
import { checkMax } from './utils/check-max.js'
|
|
16
|
+
import { western } from './utils/scale.js'
|
|
15
17
|
|
|
16
18
|
// ============================================================================
|
|
17
19
|
// Vocabulary (module-level constants)
|
|
@@ -21,14 +23,20 @@ import { parseOrdinalValue } from './utils/parse-ordinal.js'
|
|
|
21
23
|
const ONES = ['không', 'một', 'hai', 'ba', 'bốn', 'năm', 'sáu', 'bảy', 'tám', 'chín']
|
|
22
24
|
|
|
23
25
|
// Scale words indexed by scale level (0 = units, 1 = thousands, etc.)
|
|
26
|
+
// Vietnamese composes large numbers by cycling nghìn/triệu/tỷ and appending
|
|
27
|
+
// another tỷ every three groups. The recursion past tỷ tỷ (10^18) is rarely used
|
|
28
|
+
// and not firmly fixed, so the table stops at that scale word rather than invent
|
|
29
|
+
// more; the ceiling then falls where the next one would be needed (see below).
|
|
24
30
|
const SCALES = [
|
|
25
|
-
'', 'nghìn', 'triệu', 'tỷ', 'nghìn tỷ', '
|
|
26
|
-
'Quintillion', 'Sextillion', 'Septillion', 'Octillion',
|
|
27
|
-
'Nonillion', 'Decillion', 'Undecillion', 'Duodecillion',
|
|
28
|
-
'Tredecillion', 'Quattuordecillion', 'Sexdecillion',
|
|
29
|
-
'Septendecillion', 'Octodecillion', 'Novemdecillion', 'Vigintillion'
|
|
31
|
+
'', 'nghìn', 'triệu', 'tỷ', 'nghìn tỷ', 'triệu tỷ', 'tỷ tỷ',
|
|
30
32
|
]
|
|
31
33
|
|
|
34
|
+
// 3-digit grouping with SCALES[0] = '' (units); the highest scale word is
|
|
35
|
+
// tỷ tỷ at index 6 (10^18), so values from 10^(3 * SCALES.length) up have no scale word.
|
|
36
|
+
export const cardinalMax = western(SCALES.length - 1)
|
|
37
|
+
export const ordinalMax = western(SCALES.length - 1)
|
|
38
|
+
export const currencyMax = western(SCALES.length - 1)
|
|
39
|
+
|
|
32
40
|
const HUNDRED = 'trăm'
|
|
33
41
|
const ZERO = 'không'
|
|
34
42
|
const NEGATIVE = 'âm'
|
|
@@ -59,8 +67,10 @@ const LAM = 'lăm' // 5 in tens position (25, 35, etc.)
|
|
|
59
67
|
|
|
60
68
|
/**
|
|
61
69
|
* Builds word for 0-99 with special forms (mốt, lăm).
|
|
70
|
+
* @param {number} n - Integer in range 0-99
|
|
71
|
+
* @returns {string} Vietnamese words
|
|
62
72
|
*/
|
|
63
|
-
function buildBelowHundred
|
|
73
|
+
function buildBelowHundred(n) {
|
|
64
74
|
if (n === 0) return ONES[0]
|
|
65
75
|
if (n < 10) return ONES[n]
|
|
66
76
|
|
|
@@ -85,8 +95,10 @@ function buildBelowHundred (n) {
|
|
|
85
95
|
|
|
86
96
|
/**
|
|
87
97
|
* Builds segment word for 0-999.
|
|
98
|
+
* @param {number} n - Integer in range 0-999
|
|
99
|
+
* @returns {string} Vietnamese words
|
|
88
100
|
*/
|
|
89
|
-
function buildSegment
|
|
101
|
+
function buildSegment(n) {
|
|
90
102
|
if (n === 0) return ''
|
|
91
103
|
|
|
92
104
|
const hundreds = Math.trunc(n / 100)
|
|
@@ -105,10 +117,12 @@ function buildSegment (n) {
|
|
|
105
117
|
result += ' ' + LE + ' '
|
|
106
118
|
// Use "năm" not "lăm" after lẻ
|
|
107
119
|
result += remainder === 5 ? 'năm' : ONES[remainder]
|
|
108
|
-
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
109
122
|
result = ONES[remainder]
|
|
110
123
|
}
|
|
111
|
-
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
112
126
|
// 10-99 after hundreds
|
|
113
127
|
if (result) result += ' '
|
|
114
128
|
result += buildBelowHundred(remainder)
|
|
@@ -120,8 +134,10 @@ function buildSegment (n) {
|
|
|
120
134
|
|
|
121
135
|
/**
|
|
122
136
|
* Builds "lẻ" prefixed word for small remainders (1-99) after scale words.
|
|
137
|
+
* @param {number} n - Integer in range 0-99
|
|
138
|
+
* @returns {string} Vietnamese words
|
|
123
139
|
*/
|
|
124
|
-
function buildLeSegment
|
|
140
|
+
function buildLeSegment(n) {
|
|
125
141
|
if (n === 0) return ''
|
|
126
142
|
if (n < 10) {
|
|
127
143
|
// Use "năm" not "lăm" after lẻ
|
|
@@ -136,11 +152,10 @@ function buildLeSegment (n) {
|
|
|
136
152
|
|
|
137
153
|
/**
|
|
138
154
|
* Converts a non-negative integer to Vietnamese words.
|
|
139
|
-
*
|
|
140
155
|
* @param {bigint} n - Non-negative integer to convert
|
|
141
156
|
* @returns {string} Vietnamese words
|
|
142
157
|
*/
|
|
143
|
-
function integerToWords
|
|
158
|
+
function integerToWords(n) {
|
|
144
159
|
if (n === 0n) return ZERO
|
|
145
160
|
|
|
146
161
|
// Fast path: numbers < 100
|
|
@@ -178,11 +193,10 @@ function integerToWords (n) {
|
|
|
178
193
|
|
|
179
194
|
/**
|
|
180
195
|
* Builds words for numbers >= 1,000,000.
|
|
181
|
-
*
|
|
182
196
|
* @param {bigint} n - Number >= 1,000,000
|
|
183
197
|
* @returns {string} Vietnamese words
|
|
184
198
|
*/
|
|
185
|
-
function buildLargeNumberWords
|
|
199
|
+
function buildLargeNumberWords(n) {
|
|
186
200
|
const numStr = n.toString()
|
|
187
201
|
const len = numStr.length
|
|
188
202
|
|
|
@@ -212,7 +226,8 @@ function buildLargeNumberWords (n) {
|
|
|
212
226
|
if (words) {
|
|
213
227
|
if (scaleIndex > 0) {
|
|
214
228
|
parts.push(words + ' ' + SCALES[scaleIndex])
|
|
215
|
-
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
216
231
|
parts.push(words)
|
|
217
232
|
}
|
|
218
233
|
}
|
|
@@ -246,11 +261,10 @@ function buildLargeNumberWords (n) {
|
|
|
246
261
|
|
|
247
262
|
/**
|
|
248
263
|
* Converts decimal digits to Vietnamese words.
|
|
249
|
-
*
|
|
250
264
|
* @param {string} decimalPart - Decimal digits (without the point)
|
|
251
265
|
* @returns {string} Vietnamese words for decimal part
|
|
252
266
|
*/
|
|
253
|
-
function decimalPartToWords
|
|
267
|
+
function decimalPartToWords(decimalPart) {
|
|
254
268
|
let result = ''
|
|
255
269
|
|
|
256
270
|
// Handle leading zeros
|
|
@@ -276,19 +290,20 @@ function decimalPartToWords (decimalPart) {
|
|
|
276
290
|
*
|
|
277
291
|
* This is the main public API. It accepts any valid numeric input
|
|
278
292
|
* (number, string, or bigint) and handles parsing internally.
|
|
279
|
-
*
|
|
280
293
|
* @param {number | string | bigint} value - The numeric value to convert
|
|
281
294
|
* @returns {string} The number in Vietnamese words
|
|
282
295
|
* @throws {TypeError} If value is not a valid numeric type
|
|
283
296
|
* @throws {Error} If value is not a valid number format
|
|
284
|
-
*
|
|
285
297
|
* @example
|
|
286
298
|
* toCardinal(42) // 'bốn mươi hai'
|
|
287
299
|
* toCardinal(101) // 'một trăm lẻ một'
|
|
288
300
|
* toCardinal(1000000) // 'một triệu'
|
|
289
301
|
*/
|
|
290
|
-
function toCardinal
|
|
302
|
+
function toCardinal(value) {
|
|
291
303
|
const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
|
|
304
|
+
// Both the integer part and the decimal's significant digits are spelled via
|
|
305
|
+
// the scale builder, so both must clear the ceiling.
|
|
306
|
+
checkMax(integerPart, cardinalMax, decimalPart)
|
|
292
307
|
|
|
293
308
|
let result = ''
|
|
294
309
|
|
|
@@ -314,11 +329,10 @@ function toCardinal (value) {
|
|
|
314
329
|
*
|
|
315
330
|
* Vietnamese ordinals use "thứ" prefix + cardinal number.
|
|
316
331
|
* Special case: "thứ nhất" for 1st (not "thứ một").
|
|
317
|
-
*
|
|
318
332
|
* @param {bigint} n - Positive integer to convert
|
|
319
333
|
* @returns {string} Vietnamese ordinal words
|
|
320
334
|
*/
|
|
321
|
-
function integerToOrdinal
|
|
335
|
+
function integerToOrdinal(n) {
|
|
322
336
|
// Special case: 1st is "thứ nhất"
|
|
323
337
|
if (n === 1n) {
|
|
324
338
|
return ORDINAL_PREFIX + ' ' + ORDINAL_ONE
|
|
@@ -330,19 +344,19 @@ function integerToOrdinal (n) {
|
|
|
330
344
|
|
|
331
345
|
/**
|
|
332
346
|
* Converts a numeric value to Vietnamese ordinal words.
|
|
333
|
-
*
|
|
334
347
|
* @param {number | string | bigint} value - The numeric value to convert (positive integer)
|
|
335
348
|
* @returns {string} The number as ordinal words
|
|
336
349
|
* @throws {TypeError} If value is not a valid numeric type
|
|
337
350
|
* @throws {RangeError} If value is negative, zero, or has a decimal part
|
|
338
|
-
*
|
|
339
351
|
* @example
|
|
340
352
|
* toOrdinal(1) // 'thứ nhất'
|
|
341
353
|
* toOrdinal(2) // 'thứ hai'
|
|
342
354
|
* toOrdinal(10) // 'thứ mười'
|
|
343
355
|
*/
|
|
344
|
-
function toOrdinal
|
|
356
|
+
function toOrdinal(value) {
|
|
345
357
|
const integerPart = parseOrdinalValue(value)
|
|
358
|
+
// Ordinals are built from the cardinal speller, so they share its ceiling.
|
|
359
|
+
checkMax(integerPart, ordinalMax)
|
|
346
360
|
return integerToOrdinal(integerPart)
|
|
347
361
|
}
|
|
348
362
|
|
|
@@ -355,19 +369,18 @@ function toOrdinal (value) {
|
|
|
355
369
|
*
|
|
356
370
|
* Vietnamese Dong has no subunit in modern usage (xu are historical).
|
|
357
371
|
* Amounts are rounded to whole đồng.
|
|
358
|
-
*
|
|
359
372
|
* @param {number | string | bigint} value - The currency amount to convert
|
|
360
373
|
* @returns {string} The amount in Vietnamese currency words
|
|
361
374
|
* @throws {TypeError} If value is not a valid numeric type
|
|
362
375
|
* @throws {Error} If value is not a valid number format
|
|
363
|
-
*
|
|
364
376
|
* @example
|
|
365
377
|
* toCurrency(42) // 'bốn mươi hai đồng'
|
|
366
378
|
* toCurrency(1000) // 'một nghìn đồng'
|
|
367
379
|
* toCurrency(-5) // 'âm năm đồng'
|
|
368
380
|
*/
|
|
369
|
-
function toCurrency
|
|
381
|
+
function toCurrency(value) {
|
|
370
382
|
const { isNegative, dollars: dong } = parseCurrencyValue(value)
|
|
383
|
+
checkMax(dong, currencyMax)
|
|
371
384
|
|
|
372
385
|
let result = ''
|
|
373
386
|
if (isNegative) {
|