@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,30 @@
|
|
|
1
|
+
import { NumberToWordsLocale } from "./types";
|
|
2
|
+
export { NumberToWordsLocale };
|
|
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 declare function numberToWords(num: number | string, locale?: NumberToWordsLocale): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.numberToWords = numberToWords;
|
|
4
|
+
const helpers_1 = require("./helpers");
|
|
5
|
+
const locales_1 = require("./locales");
|
|
6
|
+
/**
|
|
7
|
+
* Spells out a number as words, following Vietnamese or English reading
|
|
8
|
+
* conventions (e.g. `1005` -> `"một nghìn không trăm linh năm"` in Vietnamese,
|
|
9
|
+
* `"one thousand five"` in English).
|
|
10
|
+
*
|
|
11
|
+
* Decimals are supported by passing `num` as a numeric **string** (e.g.
|
|
12
|
+
* `"1.05"`) rather than a `number` — a JS `number` can only exactly
|
|
13
|
+
* represent an integer here, so a non-integer `number` is rejected rather
|
|
14
|
+
* than silently read out with floating-point rounding artifacts (e.g.
|
|
15
|
+
* `0.1 + 0.2`). The fractional part is read one digit at a time after the
|
|
16
|
+
* locale's decimal separator word (e.g. `"1.05"` -> `"một phẩy không năm"`,
|
|
17
|
+
* `"one point zero five"`), keeping `1.05` distinct from `1.5` since a
|
|
18
|
+
* leading fractional zero changes the value. Trailing fractional zeros
|
|
19
|
+
* don't (`1.50 === 1.5`), so they're dropped — `"1.50"` reads the same as
|
|
20
|
+
* `"1.5"`.
|
|
21
|
+
*
|
|
22
|
+
* @param num - Number to convert, as an integer `number` or a numeric string
|
|
23
|
+
* (optionally with a decimal point, e.g. `"1.05"`). The integer part must be
|
|
24
|
+
* finite and within the supported range (currently up to just under 10^18,
|
|
25
|
+
* comfortably covering `Number.MAX_SAFE_INTEGER`).
|
|
26
|
+
* @param locale - Target language. Defaults to `"vi"`. To support another
|
|
27
|
+
* language, add an entry to the `definitions` registry in `locales.ts`.
|
|
28
|
+
* @returns The number spelled out in words.
|
|
29
|
+
* @throws {Error} If `num` is a non-integer `number`, an invalid numeric
|
|
30
|
+
* string, `locale` isn't registered, or the integer part exceeds the
|
|
31
|
+
* locale's supported range.
|
|
32
|
+
*/
|
|
33
|
+
function numberToWords(num, locale = "vi") {
|
|
34
|
+
const definition = locales_1.definitions[locale];
|
|
35
|
+
if (!definition) {
|
|
36
|
+
throw new Error(`numberToWords: unsupported locale "${locale}"`);
|
|
37
|
+
}
|
|
38
|
+
const normalized = (0, helpers_1.normalizeNumericInput)(num);
|
|
39
|
+
const isNegative = normalized.startsWith("-");
|
|
40
|
+
const numStr = isNegative ? normalized.slice(1) : normalized;
|
|
41
|
+
const [integerStr, rawFractionalStr] = numStr.split(".");
|
|
42
|
+
// Trailing zeros never change the fractional value (1.50 === 1.5), unlike
|
|
43
|
+
// leading zeros (0.05 !== 0.5), so only trailing zeros are safe to drop.
|
|
44
|
+
const fractionalStr = rawFractionalStr === null || rawFractionalStr === void 0 ? void 0 : rawFractionalStr.replace(/0+$/, "");
|
|
45
|
+
const integerWords = (0, helpers_1.convertIntegerPart)(Number(integerStr), definition, num, locale);
|
|
46
|
+
const words = fractionalStr
|
|
47
|
+
? `${integerWords} ${definition.decimalSeparator} ${(0, helpers_1.convertFractionalPart)(fractionalStr, definition)}`
|
|
48
|
+
: integerWords;
|
|
49
|
+
return isNegative ? `${definition.negativePrefix}${words}` : words;
|
|
50
|
+
}
|
|
@@ -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,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.definitions = void 0;
|
|
4
|
+
const enOnes = [
|
|
5
|
+
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
|
|
6
|
+
];
|
|
7
|
+
const enTeens = [
|
|
8
|
+
"ten", "eleven", "twelve", "thirteen", "fourteen",
|
|
9
|
+
"fifteen", "sixteen", "seventeen", "eighteen", "nineteen",
|
|
10
|
+
];
|
|
11
|
+
const enTens = [
|
|
12
|
+
"", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety",
|
|
13
|
+
];
|
|
14
|
+
function convertGroupEn(n) {
|
|
15
|
+
const hundredsDigit = Math.floor(n / 100);
|
|
16
|
+
const remainder = n % 100;
|
|
17
|
+
const parts = [];
|
|
18
|
+
if (hundredsDigit > 0) {
|
|
19
|
+
parts.push(`${enOnes[hundredsDigit]} hundred`);
|
|
20
|
+
}
|
|
21
|
+
if (remainder > 0) {
|
|
22
|
+
if (remainder < 10) {
|
|
23
|
+
parts.push(enOnes[remainder]);
|
|
24
|
+
}
|
|
25
|
+
else if (remainder < 20) {
|
|
26
|
+
parts.push(enTeens[remainder - 10]);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const tensDigit = Math.floor(remainder / 10);
|
|
30
|
+
const unitDigit = remainder % 10;
|
|
31
|
+
parts.push(unitDigit === 0 ? enTens[tensDigit] : `${enTens[tensDigit]}-${enOnes[unitDigit]}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return parts.join(" ");
|
|
35
|
+
}
|
|
36
|
+
const viOnes = [
|
|
37
|
+
"không", "một", "hai", "ba", "bốn", "năm", "sáu", "bảy", "tám", "chín",
|
|
38
|
+
];
|
|
39
|
+
function convertGroupVi(n, isLeadingGroup) {
|
|
40
|
+
const hundredsDigit = Math.floor(n / 100);
|
|
41
|
+
const remainder = n % 100;
|
|
42
|
+
const tensDigit = Math.floor(remainder / 10);
|
|
43
|
+
const unitDigit = remainder % 10;
|
|
44
|
+
const parts = [];
|
|
45
|
+
if (hundredsDigit > 0) {
|
|
46
|
+
parts.push(`${viOnes[hundredsDigit]} trăm`);
|
|
47
|
+
}
|
|
48
|
+
else if (!isLeadingGroup) {
|
|
49
|
+
// A zero hundreds digit is still read out ("không trăm") in every group
|
|
50
|
+
// except the leading one, e.g. 1005 -> "một nghìn không trăm linh năm".
|
|
51
|
+
parts.push("không trăm");
|
|
52
|
+
}
|
|
53
|
+
if (remainder === 0) {
|
|
54
|
+
// Nothing more to add; a fully-zero group is filtered out by the caller.
|
|
55
|
+
}
|
|
56
|
+
else if (tensDigit === 0) {
|
|
57
|
+
// "linh" (roughly "and") introduces a lone unit digit whenever a tens
|
|
58
|
+
// (or hundreds) part was already read, e.g. "trăm linh năm" (105) or
|
|
59
|
+
// "không trăm linh năm" (...005); a bare leading unit needs no "linh".
|
|
60
|
+
parts.push(hundredsDigit > 0 || !isLeadingGroup ? `linh ${viOnes[unitDigit]}` : viOnes[unitDigit]);
|
|
61
|
+
}
|
|
62
|
+
else if (tensDigit === 1) {
|
|
63
|
+
// 10-19: "mười" [+ unit], with the "năm" -> "lăm" exception for 15.
|
|
64
|
+
parts.push(unitDigit === 0 ? "mười" : unitDigit === 5 ? "mười lăm" : `mười ${viOnes[unitDigit]}`);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// 20-99: "{tensDigit} mươi" [+ unit], with "một" -> "mốt" and "năm" -> "lăm".
|
|
68
|
+
const tensWord = `${viOnes[tensDigit]} mươi`;
|
|
69
|
+
if (unitDigit === 0) {
|
|
70
|
+
parts.push(tensWord);
|
|
71
|
+
}
|
|
72
|
+
else if (unitDigit === 1) {
|
|
73
|
+
parts.push(`${tensWord} mốt`);
|
|
74
|
+
}
|
|
75
|
+
else if (unitDigit === 5) {
|
|
76
|
+
parts.push(`${tensWord} lăm`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
parts.push(`${tensWord} ${viOnes[unitDigit]}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return parts.join(" ");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Registry of per-language spelling rules. Add a new language by adding a
|
|
86
|
+
* key here (and, if its word lists warrant it, a dedicated block above) —
|
|
87
|
+
* no other file needs to change.
|
|
88
|
+
*/
|
|
89
|
+
exports.definitions = {
|
|
90
|
+
en: {
|
|
91
|
+
zero: "zero",
|
|
92
|
+
negativePrefix: "negative ",
|
|
93
|
+
decimalSeparator: "point",
|
|
94
|
+
digits: enOnes,
|
|
95
|
+
scaleWords: ["", "thousand", "million", "billion", "trillion", "quadrillion"],
|
|
96
|
+
convertGroup: convertGroupEn,
|
|
97
|
+
},
|
|
98
|
+
vi: {
|
|
99
|
+
zero: "không",
|
|
100
|
+
negativePrefix: "âm ",
|
|
101
|
+
decimalSeparator: "phẩy",
|
|
102
|
+
digits: viOnes,
|
|
103
|
+
scaleWords: ["", "nghìn", "triệu", "tỷ", "nghìn tỷ", "triệu tỷ"],
|
|
104
|
+
convertGroup: convertGroupVi,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -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,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,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.round = round;
|
|
4
|
+
/**
|
|
5
|
+
* Rounds `value` to `precision` decimal places, mainly to clean up binary
|
|
6
|
+
* floating-point noise from arithmetic (e.g. `491.66999999999996` instead of
|
|
7
|
+
* `491.67`) rather than to reduce genuine precision. `precision` defaults to
|
|
8
|
+
* 10, which is generous enough to preserve real fractional input while still
|
|
9
|
+
* clearing noise that typically appears around the 15th-17th significant
|
|
10
|
+
* digit.
|
|
11
|
+
*
|
|
12
|
+
* @param value - Number to round.
|
|
13
|
+
* @param precision - Number of decimal places to keep. Defaults to `10`.
|
|
14
|
+
* @returns `value` rounded to `precision` decimal places.
|
|
15
|
+
*/
|
|
16
|
+
function round(value, precision = 10) {
|
|
17
|
+
const factor = Math.pow(10, precision);
|
|
18
|
+
return Math.round(value * factor) / factor;
|
|
19
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for simulating Java-style enums in TypeScript.
|
|
3
|
+
*
|
|
4
|
+
* Subclasses only need to `extends BaseEnum<...>` and declare constants as
|
|
5
|
+
* `static readonly Xxx = new SubClass(value, label, opts?)`, without
|
|
6
|
+
* redeclaring a constructor. Because `BaseEnum`'s constructor is `protected`,
|
|
7
|
+
* subclasses inherit it while keeping the same protection — instances can't
|
|
8
|
+
* be `new`-ed from outside the class, which preserves enum singleton/identity
|
|
9
|
+
* semantics (`===` comparisons always work as expected).
|
|
10
|
+
*
|
|
11
|
+
* @typeParam T - Type of the `value` field (defaults to `number`).
|
|
12
|
+
*/
|
|
13
|
+
export declare abstract class BaseEnum<T = number> {
|
|
14
|
+
readonly value: T;
|
|
15
|
+
readonly label: string;
|
|
16
|
+
readonly opts?: Record<string, any> | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Registry of every instance created, keyed per subclass and per `value`.
|
|
19
|
+
* The outer key is the subclass constructor (so each subclass has its own
|
|
20
|
+
* list), the inner key is each constant's `value`. Backs `values()`,
|
|
21
|
+
* `fromValue()` and `equals()`.
|
|
22
|
+
*/
|
|
23
|
+
private static readonly registry;
|
|
24
|
+
/**
|
|
25
|
+
* Creates an enum constant. Only callable from within a subclass
|
|
26
|
+
* (the constructor is `protected`), typically from a `static readonly`
|
|
27
|
+
* field declaration.
|
|
28
|
+
*
|
|
29
|
+
* @param value - Identifying value of the constant (used by `fromValue()`, `equals()`).
|
|
30
|
+
* @param label - Display label/description of the constant.
|
|
31
|
+
* @param opts - Optional extra data, freely defined by the subclass as needed.
|
|
32
|
+
*/
|
|
33
|
+
protected constructor(value: T, label: string, opts?: Record<string, any> | undefined);
|
|
34
|
+
/**
|
|
35
|
+
* Returns the names of the `static readonly` fields declared on the
|
|
36
|
+
* subclass, in declaration order. Mirrors the idea of an enum constant's
|
|
37
|
+
* name in Java, but here returns the names for every constant at once.
|
|
38
|
+
*
|
|
39
|
+
* @returns Array of constant names, e.g. `['Admin', 'User']`.
|
|
40
|
+
*/
|
|
41
|
+
static names(this: Function): string[];
|
|
42
|
+
/**
|
|
43
|
+
* Returns every instance (constant) created on the subclass, similar to
|
|
44
|
+
* Java's `Enum.values()`.
|
|
45
|
+
*
|
|
46
|
+
* @returns Array of the subclass's instances, in creation order.
|
|
47
|
+
*/
|
|
48
|
+
static values<T extends BaseEnum<any>>(this: Function & {
|
|
49
|
+
prototype: T;
|
|
50
|
+
}): T[];
|
|
51
|
+
/**
|
|
52
|
+
* Looks up a constant by its declared field name (matches Java's standard
|
|
53
|
+
* `Enum.valueOf(String)`). Unlike `fromValue()`, which looks up by `value`,
|
|
54
|
+
* this looks up by the static field's name (key).
|
|
55
|
+
*
|
|
56
|
+
* @param name - Name of the constant to look up, e.g. `'Admin'`.
|
|
57
|
+
* @returns The matching instance.
|
|
58
|
+
* @throws {Error} If no constant with that name exists.
|
|
59
|
+
*/
|
|
60
|
+
static valueOf<T extends BaseEnum<any>>(this: Function & {
|
|
61
|
+
prototype: T;
|
|
62
|
+
}, name: string): T;
|
|
63
|
+
/**
|
|
64
|
+
* Looks up a constant by its `value` field.
|
|
65
|
+
*
|
|
66
|
+
* Note: `value`'s type isn't tied to the subclass's own `value` type
|
|
67
|
+
* parameter (unlike a conditional type would give) — that syntax requires
|
|
68
|
+
* TypeScript 2.8+, and this library targets TypeScript 2.7 and up.
|
|
69
|
+
*
|
|
70
|
+
* @param value - Value to look up (of the subclass's `T` type).
|
|
71
|
+
* @returns The matching instance, or `undefined` if none is found.
|
|
72
|
+
*/
|
|
73
|
+
static fromValue<T extends BaseEnum<any>>(this: Function & {
|
|
74
|
+
prototype: T;
|
|
75
|
+
}, value: any): T | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Compares this constant against an arbitrary value.
|
|
78
|
+
*
|
|
79
|
+
* - If `other` is a `BaseEnum` instance: compares identity (`===`)
|
|
80
|
+
* directly, even if `other` belongs to a different enum class (always
|
|
81
|
+
* `false` in that case).
|
|
82
|
+
* - If `other` is a raw value (number/string/...): looks up the matching
|
|
83
|
+
* constant by `value` within this instance's own subclass, then compares
|
|
84
|
+
* identity.
|
|
85
|
+
* - Anything else (wrong type, no match, `null`/`undefined`...): returns
|
|
86
|
+
* `false`.
|
|
87
|
+
*
|
|
88
|
+
* @param other - Value or enum instance to compare against.
|
|
89
|
+
* @returns `true` if both refer to the same enum constant, otherwise `false`.
|
|
90
|
+
*/
|
|
91
|
+
equals(other: any): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Returns the field name this instance was assigned to, similar to Java's
|
|
94
|
+
* `Enum.name()`.
|
|
95
|
+
*
|
|
96
|
+
* @returns The constant's name, e.g. `'Admin'`; an empty string if not
|
|
97
|
+
* found (a theoretical case that shouldn't occur under normal usage).
|
|
98
|
+
*/
|
|
99
|
+
name(): string;
|
|
100
|
+
/**
|
|
101
|
+
* Converts the constant to a display string, defaulting to `label`.
|
|
102
|
+
*
|
|
103
|
+
* @returns The constant's display label.
|
|
104
|
+
*/
|
|
105
|
+
toString(): string;
|
|
106
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for simulating Java-style enums in TypeScript.
|
|
3
|
+
*
|
|
4
|
+
* Subclasses only need to `extends BaseEnum<...>` and declare constants as
|
|
5
|
+
* `static readonly Xxx = new SubClass(value, label, opts?)`, without
|
|
6
|
+
* redeclaring a constructor. Because `BaseEnum`'s constructor is `protected`,
|
|
7
|
+
* subclasses inherit it while keeping the same protection — instances can't
|
|
8
|
+
* be `new`-ed from outside the class, which preserves enum singleton/identity
|
|
9
|
+
* semantics (`===` comparisons always work as expected).
|
|
10
|
+
*
|
|
11
|
+
* @typeParam T - Type of the `value` field (defaults to `number`).
|
|
12
|
+
*/
|
|
13
|
+
export class BaseEnum {
|
|
14
|
+
/**
|
|
15
|
+
* Creates an enum constant. Only callable from within a subclass
|
|
16
|
+
* (the constructor is `protected`), typically from a `static readonly`
|
|
17
|
+
* field declaration.
|
|
18
|
+
*
|
|
19
|
+
* @param value - Identifying value of the constant (used by `fromValue()`, `equals()`).
|
|
20
|
+
* @param label - Display label/description of the constant.
|
|
21
|
+
* @param opts - Optional extra data, freely defined by the subclass as needed.
|
|
22
|
+
*/
|
|
23
|
+
constructor(value, label, opts) {
|
|
24
|
+
this.value = value;
|
|
25
|
+
this.label = label;
|
|
26
|
+
this.opts = opts;
|
|
27
|
+
let map = BaseEnum.registry.get(this.constructor);
|
|
28
|
+
if (!map) {
|
|
29
|
+
map = new Map();
|
|
30
|
+
BaseEnum.registry.set(this.constructor, map);
|
|
31
|
+
}
|
|
32
|
+
map.set(value, this);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Returns the names of the `static readonly` fields declared on the
|
|
36
|
+
* subclass, in declaration order. Mirrors the idea of an enum constant's
|
|
37
|
+
* name in Java, but here returns the names for every constant at once.
|
|
38
|
+
*
|
|
39
|
+
* @returns Array of constant names, e.g. `['Admin', 'User']`.
|
|
40
|
+
*/
|
|
41
|
+
static names() {
|
|
42
|
+
return Object.getOwnPropertyNames(this).filter((key) => key !== "prototype" && this[key] instanceof BaseEnum);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns every instance (constant) created on the subclass, similar to
|
|
46
|
+
* Java's `Enum.values()`.
|
|
47
|
+
*
|
|
48
|
+
* @returns Array of the subclass's instances, in creation order.
|
|
49
|
+
*/
|
|
50
|
+
static values() {
|
|
51
|
+
var _a;
|
|
52
|
+
return Array.from(((_a = BaseEnum.registry.get(this)) !== null && _a !== void 0 ? _a : new Map()).values());
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Looks up a constant by its declared field name (matches Java's standard
|
|
56
|
+
* `Enum.valueOf(String)`). Unlike `fromValue()`, which looks up by `value`,
|
|
57
|
+
* this looks up by the static field's name (key).
|
|
58
|
+
*
|
|
59
|
+
* @param name - Name of the constant to look up, e.g. `'Admin'`.
|
|
60
|
+
* @returns The matching instance.
|
|
61
|
+
* @throws {Error} If no constant with that name exists.
|
|
62
|
+
*/
|
|
63
|
+
static valueOf(name) {
|
|
64
|
+
const constant = this[name];
|
|
65
|
+
if (!(constant instanceof BaseEnum)) {
|
|
66
|
+
throw new Error(`No enum constant ${this.name}.${name}`);
|
|
67
|
+
}
|
|
68
|
+
return constant;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Looks up a constant by its `value` field.
|
|
72
|
+
*
|
|
73
|
+
* Note: `value`'s type isn't tied to the subclass's own `value` type
|
|
74
|
+
* parameter (unlike a conditional type would give) — that syntax requires
|
|
75
|
+
* TypeScript 2.8+, and this library targets TypeScript 2.7 and up.
|
|
76
|
+
*
|
|
77
|
+
* @param value - Value to look up (of the subclass's `T` type).
|
|
78
|
+
* @returns The matching instance, or `undefined` if none is found.
|
|
79
|
+
*/
|
|
80
|
+
static fromValue(value) {
|
|
81
|
+
var _a;
|
|
82
|
+
return (_a = BaseEnum.registry.get(this)) === null || _a === void 0 ? void 0 : _a.get(value);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Compares this constant against an arbitrary value.
|
|
86
|
+
*
|
|
87
|
+
* - If `other` is a `BaseEnum` instance: compares identity (`===`)
|
|
88
|
+
* directly, even if `other` belongs to a different enum class (always
|
|
89
|
+
* `false` in that case).
|
|
90
|
+
* - If `other` is a raw value (number/string/...): looks up the matching
|
|
91
|
+
* constant by `value` within this instance's own subclass, then compares
|
|
92
|
+
* identity.
|
|
93
|
+
* - Anything else (wrong type, no match, `null`/`undefined`...): returns
|
|
94
|
+
* `false`.
|
|
95
|
+
*
|
|
96
|
+
* @param other - Value or enum instance to compare against.
|
|
97
|
+
* @returns `true` if both refer to the same enum constant, otherwise `false`.
|
|
98
|
+
*/
|
|
99
|
+
equals(other) {
|
|
100
|
+
var _a;
|
|
101
|
+
if (other instanceof BaseEnum) {
|
|
102
|
+
return this === other;
|
|
103
|
+
}
|
|
104
|
+
return ((_a = BaseEnum.registry.get(this.constructor)) === null || _a === void 0 ? void 0 : _a.get(other)) === this;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Returns the field name this instance was assigned to, similar to Java's
|
|
108
|
+
* `Enum.name()`.
|
|
109
|
+
*
|
|
110
|
+
* @returns The constant's name, e.g. `'Admin'`; an empty string if not
|
|
111
|
+
* found (a theoretical case that shouldn't occur under normal usage).
|
|
112
|
+
*/
|
|
113
|
+
name() {
|
|
114
|
+
const ctor = this.constructor;
|
|
115
|
+
const key = Object.getOwnPropertyNames(ctor).find((k) => k !== "prototype" && ctor[k] === this);
|
|
116
|
+
return key !== null && key !== void 0 ? key : "";
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Converts the constant to a display string, defaulting to `label`.
|
|
120
|
+
*
|
|
121
|
+
* @returns The constant's display label.
|
|
122
|
+
*/
|
|
123
|
+
toString() {
|
|
124
|
+
return this.label;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Registry of every instance created, keyed per subclass and per `value`.
|
|
129
|
+
* The outer key is the subclass constructor (so each subclass has its own
|
|
130
|
+
* list), the inner key is each constant's `value`. Backs `values()`,
|
|
131
|
+
* `fromValue()` and `equals()`.
|
|
132
|
+
*/
|
|
133
|
+
BaseEnum.registry = new Map();
|