@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.
Files changed (37) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +113 -0
  3. package/dist/cjs/base-enum.d.ts +106 -0
  4. package/dist/cjs/base-enum.js +137 -0
  5. package/dist/cjs/func.d.ts +227 -0
  6. package/dist/cjs/func.js +574 -0
  7. package/dist/cjs/index.d.ts +4 -0
  8. package/dist/cjs/index.js +20 -0
  9. package/dist/cjs/number-to-words/helpers.d.ts +17 -0
  10. package/dist/cjs/number-to-words/helpers.js +72 -0
  11. package/dist/cjs/number-to-words/index.d.ts +30 -0
  12. package/dist/cjs/number-to-words/index.js +50 -0
  13. package/dist/cjs/number-to-words/locales.d.ts +7 -0
  14. package/dist/cjs/number-to-words/locales.js +106 -0
  15. package/dist/cjs/number-to-words/types.d.ts +35 -0
  16. package/dist/cjs/number-to-words/types.js +2 -0
  17. package/dist/cjs/package.json +4 -0
  18. package/dist/cjs/round.d.ts +13 -0
  19. package/dist/cjs/round.js +19 -0
  20. package/dist/esm/base-enum.d.ts +106 -0
  21. package/dist/esm/base-enum.js +133 -0
  22. package/dist/esm/func.d.ts +227 -0
  23. package/dist/esm/func.js +547 -0
  24. package/dist/esm/index.d.ts +4 -0
  25. package/dist/esm/index.js +4 -0
  26. package/dist/esm/number-to-words/helpers.d.ts +17 -0
  27. package/dist/esm/number-to-words/helpers.js +67 -0
  28. package/dist/esm/number-to-words/index.d.ts +30 -0
  29. package/dist/esm/number-to-words/index.js +47 -0
  30. package/dist/esm/number-to-words/locales.d.ts +7 -0
  31. package/dist/esm/number-to-words/locales.js +103 -0
  32. package/dist/esm/number-to-words/types.d.ts +35 -0
  33. package/dist/esm/number-to-words/types.js +1 -0
  34. package/dist/esm/package.json +4 -0
  35. package/dist/esm/round.d.ts +13 -0
  36. package/dist/esm/round.js +16 -0
  37. 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,4 @@
1
+ {
2
+ "type": "module",
3
+ "sideEffects": false
4
+ }
@@ -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
+ }