@khgtrn/lib 1.0.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/LICENSE +7 -0
- package/README.md +113 -0
- package/dist/cjs/base-enum.d.ts +106 -0
- package/dist/cjs/base-enum.js +137 -0
- package/dist/cjs/func.d.ts +227 -0
- package/dist/cjs/func.js +574 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/number-to-words/helpers.d.ts +17 -0
- package/dist/cjs/number-to-words/helpers.js +72 -0
- package/dist/cjs/number-to-words/index.d.ts +30 -0
- package/dist/cjs/number-to-words/index.js +50 -0
- package/dist/cjs/number-to-words/locales.d.ts +7 -0
- package/dist/cjs/number-to-words/locales.js +106 -0
- package/dist/cjs/number-to-words/types.d.ts +35 -0
- package/dist/cjs/number-to-words/types.js +2 -0
- package/dist/cjs/package.json +4 -0
- package/dist/cjs/round.d.ts +13 -0
- package/dist/cjs/round.js +19 -0
- package/dist/esm/base-enum.d.ts +106 -0
- package/dist/esm/base-enum.js +133 -0
- package/dist/esm/func.d.ts +227 -0
- package/dist/esm/func.js +547 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/number-to-words/helpers.d.ts +17 -0
- package/dist/esm/number-to-words/helpers.js +67 -0
- package/dist/esm/number-to-words/index.d.ts +30 -0
- package/dist/esm/number-to-words/index.js +47 -0
- package/dist/esm/number-to-words/locales.d.ts +7 -0
- package/dist/esm/number-to-words/locales.js +103 -0
- package/dist/esm/number-to-words/types.d.ts +35 -0
- package/dist/esm/number-to-words/types.js +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/round.d.ts +13 -0
- package/dist/esm/round.js +16 -0
- package/package.json +50 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { convertFractionalPart, convertIntegerPart, normalizeNumericInput } from "./helpers";
|
|
2
|
+
import { definitions } from "./locales";
|
|
3
|
+
/**
|
|
4
|
+
* Spells out a number as words, following Vietnamese or English reading
|
|
5
|
+
* conventions (e.g. `1005` -> `"một nghìn không trăm linh năm"` in Vietnamese,
|
|
6
|
+
* `"one thousand five"` in English).
|
|
7
|
+
*
|
|
8
|
+
* Decimals are supported by passing `num` as a numeric **string** (e.g.
|
|
9
|
+
* `"1.05"`) rather than a `number` — a JS `number` can only exactly
|
|
10
|
+
* represent an integer here, so a non-integer `number` is rejected rather
|
|
11
|
+
* than silently read out with floating-point rounding artifacts (e.g.
|
|
12
|
+
* `0.1 + 0.2`). The fractional part is read one digit at a time after the
|
|
13
|
+
* locale's decimal separator word (e.g. `"1.05"` -> `"một phẩy không năm"`,
|
|
14
|
+
* `"one point zero five"`), keeping `1.05` distinct from `1.5` since a
|
|
15
|
+
* leading fractional zero changes the value. Trailing fractional zeros
|
|
16
|
+
* don't (`1.50 === 1.5`), so they're dropped — `"1.50"` reads the same as
|
|
17
|
+
* `"1.5"`.
|
|
18
|
+
*
|
|
19
|
+
* @param num - Number to convert, as an integer `number` or a numeric string
|
|
20
|
+
* (optionally with a decimal point, e.g. `"1.05"`). The integer part must be
|
|
21
|
+
* finite and within the supported range (currently up to just under 10^18,
|
|
22
|
+
* comfortably covering `Number.MAX_SAFE_INTEGER`).
|
|
23
|
+
* @param locale - Target language. Defaults to `"vi"`. To support another
|
|
24
|
+
* language, add an entry to the `definitions` registry in `locales.ts`.
|
|
25
|
+
* @returns The number spelled out in words.
|
|
26
|
+
* @throws {Error} If `num` is a non-integer `number`, an invalid numeric
|
|
27
|
+
* string, `locale` isn't registered, or the integer part exceeds the
|
|
28
|
+
* locale's supported range.
|
|
29
|
+
*/
|
|
30
|
+
export function numberToWords(num, locale = "vi") {
|
|
31
|
+
const definition = definitions[locale];
|
|
32
|
+
if (!definition) {
|
|
33
|
+
throw new Error(`numberToWords: unsupported locale "${locale}"`);
|
|
34
|
+
}
|
|
35
|
+
const normalized = normalizeNumericInput(num);
|
|
36
|
+
const isNegative = normalized.startsWith("-");
|
|
37
|
+
const numStr = isNegative ? normalized.slice(1) : normalized;
|
|
38
|
+
const [integerStr, rawFractionalStr] = numStr.split(".");
|
|
39
|
+
// Trailing zeros never change the fractional value (1.50 === 1.5), unlike
|
|
40
|
+
// leading zeros (0.05 !== 0.5), so only trailing zeros are safe to drop.
|
|
41
|
+
const fractionalStr = rawFractionalStr === null || rawFractionalStr === void 0 ? void 0 : rawFractionalStr.replace(/0+$/, "");
|
|
42
|
+
const integerWords = convertIntegerPart(Number(integerStr), definition, num, locale);
|
|
43
|
+
const words = fractionalStr
|
|
44
|
+
? `${integerWords} ${definition.decimalSeparator} ${convertFractionalPart(fractionalStr, definition)}`
|
|
45
|
+
: integerWords;
|
|
46
|
+
return isNegative ? `${definition.negativePrefix}${words}` : words;
|
|
47
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { NumberToWordsDefinition, NumberToWordsLocale } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Registry of per-language spelling rules. Add a new language by adding a
|
|
4
|
+
* key here (and, if its word lists warrant it, a dedicated block above) —
|
|
5
|
+
* no other file needs to change.
|
|
6
|
+
*/
|
|
7
|
+
export declare const definitions: Record<NumberToWordsLocale, NumberToWordsDefinition>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const enOnes = [
|
|
2
|
+
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
|
|
3
|
+
];
|
|
4
|
+
const enTeens = [
|
|
5
|
+
"ten", "eleven", "twelve", "thirteen", "fourteen",
|
|
6
|
+
"fifteen", "sixteen", "seventeen", "eighteen", "nineteen",
|
|
7
|
+
];
|
|
8
|
+
const enTens = [
|
|
9
|
+
"", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety",
|
|
10
|
+
];
|
|
11
|
+
function convertGroupEn(n) {
|
|
12
|
+
const hundredsDigit = Math.floor(n / 100);
|
|
13
|
+
const remainder = n % 100;
|
|
14
|
+
const parts = [];
|
|
15
|
+
if (hundredsDigit > 0) {
|
|
16
|
+
parts.push(`${enOnes[hundredsDigit]} hundred`);
|
|
17
|
+
}
|
|
18
|
+
if (remainder > 0) {
|
|
19
|
+
if (remainder < 10) {
|
|
20
|
+
parts.push(enOnes[remainder]);
|
|
21
|
+
}
|
|
22
|
+
else if (remainder < 20) {
|
|
23
|
+
parts.push(enTeens[remainder - 10]);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const tensDigit = Math.floor(remainder / 10);
|
|
27
|
+
const unitDigit = remainder % 10;
|
|
28
|
+
parts.push(unitDigit === 0 ? enTens[tensDigit] : `${enTens[tensDigit]}-${enOnes[unitDigit]}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return parts.join(" ");
|
|
32
|
+
}
|
|
33
|
+
const viOnes = [
|
|
34
|
+
"không", "một", "hai", "ba", "bốn", "năm", "sáu", "bảy", "tám", "chín",
|
|
35
|
+
];
|
|
36
|
+
function convertGroupVi(n, isLeadingGroup) {
|
|
37
|
+
const hundredsDigit = Math.floor(n / 100);
|
|
38
|
+
const remainder = n % 100;
|
|
39
|
+
const tensDigit = Math.floor(remainder / 10);
|
|
40
|
+
const unitDigit = remainder % 10;
|
|
41
|
+
const parts = [];
|
|
42
|
+
if (hundredsDigit > 0) {
|
|
43
|
+
parts.push(`${viOnes[hundredsDigit]} trăm`);
|
|
44
|
+
}
|
|
45
|
+
else if (!isLeadingGroup) {
|
|
46
|
+
// A zero hundreds digit is still read out ("không trăm") in every group
|
|
47
|
+
// except the leading one, e.g. 1005 -> "một nghìn không trăm linh năm".
|
|
48
|
+
parts.push("không trăm");
|
|
49
|
+
}
|
|
50
|
+
if (remainder === 0) {
|
|
51
|
+
// Nothing more to add; a fully-zero group is filtered out by the caller.
|
|
52
|
+
}
|
|
53
|
+
else if (tensDigit === 0) {
|
|
54
|
+
// "linh" (roughly "and") introduces a lone unit digit whenever a tens
|
|
55
|
+
// (or hundreds) part was already read, e.g. "trăm linh năm" (105) or
|
|
56
|
+
// "không trăm linh năm" (...005); a bare leading unit needs no "linh".
|
|
57
|
+
parts.push(hundredsDigit > 0 || !isLeadingGroup ? `linh ${viOnes[unitDigit]}` : viOnes[unitDigit]);
|
|
58
|
+
}
|
|
59
|
+
else if (tensDigit === 1) {
|
|
60
|
+
// 10-19: "mười" [+ unit], with the "năm" -> "lăm" exception for 15.
|
|
61
|
+
parts.push(unitDigit === 0 ? "mười" : unitDigit === 5 ? "mười lăm" : `mười ${viOnes[unitDigit]}`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// 20-99: "{tensDigit} mươi" [+ unit], with "một" -> "mốt" and "năm" -> "lăm".
|
|
65
|
+
const tensWord = `${viOnes[tensDigit]} mươi`;
|
|
66
|
+
if (unitDigit === 0) {
|
|
67
|
+
parts.push(tensWord);
|
|
68
|
+
}
|
|
69
|
+
else if (unitDigit === 1) {
|
|
70
|
+
parts.push(`${tensWord} mốt`);
|
|
71
|
+
}
|
|
72
|
+
else if (unitDigit === 5) {
|
|
73
|
+
parts.push(`${tensWord} lăm`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
parts.push(`${tensWord} ${viOnes[unitDigit]}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return parts.join(" ");
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Registry of per-language spelling rules. Add a new language by adding a
|
|
83
|
+
* key here (and, if its word lists warrant it, a dedicated block above) —
|
|
84
|
+
* no other file needs to change.
|
|
85
|
+
*/
|
|
86
|
+
export const definitions = {
|
|
87
|
+
en: {
|
|
88
|
+
zero: "zero",
|
|
89
|
+
negativePrefix: "negative ",
|
|
90
|
+
decimalSeparator: "point",
|
|
91
|
+
digits: enOnes,
|
|
92
|
+
scaleWords: ["", "thousand", "million", "billion", "trillion", "quadrillion"],
|
|
93
|
+
convertGroup: convertGroupEn,
|
|
94
|
+
},
|
|
95
|
+
vi: {
|
|
96
|
+
zero: "không",
|
|
97
|
+
negativePrefix: "âm ",
|
|
98
|
+
decimalSeparator: "phẩy",
|
|
99
|
+
digits: viOnes,
|
|
100
|
+
scaleWords: ["", "nghìn", "triệu", "tỷ", "nghìn tỷ", "triệu tỷ"],
|
|
101
|
+
convertGroup: convertGroupVi,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale code supported by {@link numberToWords}. Extend by adding a new key
|
|
3
|
+
* to the `definitions` registry in `locales.ts` (see
|
|
4
|
+
* {@link NumberToWordsDefinition}) — no changes to `numberToWords` itself
|
|
5
|
+
* are needed to add a language.
|
|
6
|
+
*/
|
|
7
|
+
export type NumberToWordsLocale = "vi" | "en";
|
|
8
|
+
/**
|
|
9
|
+
* Per-locale rules needed to spell out numbers.
|
|
10
|
+
*
|
|
11
|
+
* Numbers are split into groups of 3 digits (thousands grouping), most
|
|
12
|
+
* significant group first. `scaleWords[i]` is the word placed after a group
|
|
13
|
+
* at position `i` counting from the right (`0` = units group, which gets no
|
|
14
|
+
* scale word; `1` = thousand-level; `2` = million-level; and so on).
|
|
15
|
+
*/
|
|
16
|
+
export interface NumberToWordsDefinition {
|
|
17
|
+
/** Word for the number `0` on its own. */
|
|
18
|
+
zero: string;
|
|
19
|
+
/** Prefix used for negative numbers (including trailing space, if any). */
|
|
20
|
+
negativePrefix: string;
|
|
21
|
+
/** Word placed between the integer and fractional parts, e.g. "phẩy"/"point". */
|
|
22
|
+
decimalSeparator: string;
|
|
23
|
+
/** Word for each digit `0`-`9`, used to read the fractional part one digit at a time. */
|
|
24
|
+
digits: string[];
|
|
25
|
+
/** Scale words indexed by group position: `["", "thousand", "million", ...]`. */
|
|
26
|
+
scaleWords: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Spells out a single 0-999 group.
|
|
29
|
+
* @param n - Group value, `1`-`999` (the caller never invokes this for `0`).
|
|
30
|
+
* @param isLeadingGroup - `true` for the most significant non-zero group of
|
|
31
|
+
* the whole number. Some languages (e.g. Vietnamese) read a zero hundreds
|
|
32
|
+
* digit explicitly ("không trăm") in every group except the leading one.
|
|
33
|
+
*/
|
|
34
|
+
convertGroup(n: number, isLeadingGroup: boolean): string;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rounds `value` to `precision` decimal places, mainly to clean up binary
|
|
3
|
+
* floating-point noise from arithmetic (e.g. `491.66999999999996` instead of
|
|
4
|
+
* `491.67`) rather than to reduce genuine precision. `precision` defaults to
|
|
5
|
+
* 10, which is generous enough to preserve real fractional input while still
|
|
6
|
+
* clearing noise that typically appears around the 15th-17th significant
|
|
7
|
+
* digit.
|
|
8
|
+
*
|
|
9
|
+
* @param value - Number to round.
|
|
10
|
+
* @param precision - Number of decimal places to keep. Defaults to `10`.
|
|
11
|
+
* @returns `value` rounded to `precision` decimal places.
|
|
12
|
+
*/
|
|
13
|
+
export declare function round(value: number, precision?: number): number;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rounds `value` to `precision` decimal places, mainly to clean up binary
|
|
3
|
+
* floating-point noise from arithmetic (e.g. `491.66999999999996` instead of
|
|
4
|
+
* `491.67`) rather than to reduce genuine precision. `precision` defaults to
|
|
5
|
+
* 10, which is generous enough to preserve real fractional input while still
|
|
6
|
+
* clearing noise that typically appears around the 15th-17th significant
|
|
7
|
+
* digit.
|
|
8
|
+
*
|
|
9
|
+
* @param value - Number to round.
|
|
10
|
+
* @param precision - Number of decimal places to keep. Defaults to `10`.
|
|
11
|
+
* @returns `value` rounded to `precision` decimal places.
|
|
12
|
+
*/
|
|
13
|
+
export function round(value, precision = 10) {
|
|
14
|
+
const factor = Math.pow(10, precision);
|
|
15
|
+
return Math.round(value * factor) / factor;
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@khgtrn/lib",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Library for Typescript",
|
|
5
|
+
"main": "./dist/cjs/index.js",
|
|
6
|
+
"module": "./dist/esm/index.js",
|
|
7
|
+
"types": "./dist/cjs/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/cjs/index.d.ts",
|
|
11
|
+
"require": "./dist/cjs/index.js",
|
|
12
|
+
"import": "./dist/esm/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=19"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"typescript",
|
|
23
|
+
"ts",
|
|
24
|
+
"lib",
|
|
25
|
+
"library",
|
|
26
|
+
"klib"
|
|
27
|
+
],
|
|
28
|
+
"author": "khgtrn",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"devEngines": {
|
|
31
|
+
"packageManager": {
|
|
32
|
+
"name": "pnpm",
|
|
33
|
+
"version": "12.3.4",
|
|
34
|
+
"onFail": "download"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"tsx": "^4.23.13",
|
|
39
|
+
"typescript": "^7.0.2"
|
|
40
|
+
},
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"scripts": {
|
|
43
|
+
"clean": "rm -rf dist",
|
|
44
|
+
"build:esm": "tsc -p tsconfig.json",
|
|
45
|
+
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
46
|
+
"postbuild": "cp config/package-cjs.json dist/cjs/package.json && cp config/package-esm.json dist/esm/package.json",
|
|
47
|
+
"build": "pnpm run clean && pnpm run build:esm && pnpm run build:cjs && pnpm run postbuild",
|
|
48
|
+
"test": "tsx tests/index.ts"
|
|
49
|
+
}
|
|
50
|
+
}
|