@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
package/dist/cjs/func.js
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isEmpty = isEmpty;
|
|
4
|
+
exports.isNumber = isNumber;
|
|
5
|
+
exports.vi2en = vi2en;
|
|
6
|
+
exports.crlf2lf = crlf2lf;
|
|
7
|
+
exports.removeNewline = removeNewline;
|
|
8
|
+
exports.shuffleArray = shuffleArray;
|
|
9
|
+
exports.objectValueToArray = objectValueToArray;
|
|
10
|
+
exports.groupBy = groupBy;
|
|
11
|
+
exports.removeByKey = removeByKey;
|
|
12
|
+
exports.removeEmptyValue = removeEmptyValue;
|
|
13
|
+
exports.randomString = randomString;
|
|
14
|
+
exports.byte2hex = byte2hex;
|
|
15
|
+
exports.uuid7bin = uuid7bin;
|
|
16
|
+
exports.uuid7 = uuid7;
|
|
17
|
+
exports.base64encode = base64encode;
|
|
18
|
+
exports.base64decode = base64decode;
|
|
19
|
+
exports.getObjectValue = getObjectValue;
|
|
20
|
+
exports.ov = ov;
|
|
21
|
+
exports.toInt = toInt;
|
|
22
|
+
exports.numberToRoman = numberToRoman;
|
|
23
|
+
exports.romanToNumber = romanToNumber;
|
|
24
|
+
exports.isRomanNumber = isRomanNumber;
|
|
25
|
+
exports.base64ToBlob = base64ToBlob;
|
|
26
|
+
exports.downloadFile = downloadFile;
|
|
27
|
+
exports.deepClone = deepClone;
|
|
28
|
+
/**
|
|
29
|
+
* Checks whether a value is considered "empty".
|
|
30
|
+
*
|
|
31
|
+
* - string: empty after trimming.
|
|
32
|
+
* - number: equal to `0`.
|
|
33
|
+
* - boolean: `false`.
|
|
34
|
+
* - `null`/`undefined`: always empty.
|
|
35
|
+
* - array: length `0`.
|
|
36
|
+
* - other objects: no own enumerable keys.
|
|
37
|
+
* - anything else (function, symbol, ...): never empty.
|
|
38
|
+
*
|
|
39
|
+
* @param value - Value to check.
|
|
40
|
+
* @returns `true` if the value is considered empty.
|
|
41
|
+
*/
|
|
42
|
+
function isEmpty(value) {
|
|
43
|
+
if (typeof value === "string") {
|
|
44
|
+
return value.trim() === "";
|
|
45
|
+
}
|
|
46
|
+
else if (typeof value === "number") {
|
|
47
|
+
return value === 0;
|
|
48
|
+
}
|
|
49
|
+
else if (typeof value === "boolean") {
|
|
50
|
+
return !value;
|
|
51
|
+
}
|
|
52
|
+
else if (value === undefined || value === null) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
else if (Array.isArray(value)) {
|
|
56
|
+
return value.length === 0;
|
|
57
|
+
}
|
|
58
|
+
else if (typeof value === "object") {
|
|
59
|
+
return Object.keys(value).length === 0;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Checks whether a value is a valid number (not `NaN`, not `Infinity`).
|
|
65
|
+
* @param value - Value to check.
|
|
66
|
+
* @returns Type-guard: `true` if `value` is a finite number.
|
|
67
|
+
*/
|
|
68
|
+
function isNumber(value) {
|
|
69
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Converts Vietnamese diacritics to their plain ASCII equivalents
|
|
73
|
+
* (e.g. `"Điều chỉnh"` -> `"Dieu chinh"`).
|
|
74
|
+
*
|
|
75
|
+
* @param s - Input string.
|
|
76
|
+
* @returns The string with Vietnamese diacritics removed.
|
|
77
|
+
*/
|
|
78
|
+
function vi2en(s) {
|
|
79
|
+
return (s
|
|
80
|
+
// Decompose accented characters into base character + combining mark
|
|
81
|
+
.normalize("NFD")
|
|
82
|
+
// Strip all combining diacritical marks
|
|
83
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
84
|
+
// đ/Đ have no combining-mark decomposition, handle them separately
|
|
85
|
+
.replace(/đ/g, "d")
|
|
86
|
+
.replace(/Đ/g, "D"));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Normalizes Windows-style line endings (`\r\n`) to Unix-style (`\n`).
|
|
90
|
+
* @param value - Input string.
|
|
91
|
+
* @returns The string with all `\r\n` replaced by `\n`.
|
|
92
|
+
*/
|
|
93
|
+
function crlf2lf(value) {
|
|
94
|
+
return value.replace(/\r\n/g, "\n");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Removes every newline from a string, after normalizing line endings and
|
|
98
|
+
* trimming surrounding whitespace.
|
|
99
|
+
* @param value - Input string.
|
|
100
|
+
* @returns The string with all newlines removed.
|
|
101
|
+
*/
|
|
102
|
+
function removeNewline(value) {
|
|
103
|
+
return crlf2lf(value).trim().replace(/\n/g, "");
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Shuffles an array in place using the Fisher-Yates algorithm.
|
|
107
|
+
* @param array - Array to shuffle (mutated directly).
|
|
108
|
+
* @returns The same array reference, shuffled.
|
|
109
|
+
*/
|
|
110
|
+
function shuffleArray(array) {
|
|
111
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
112
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
113
|
+
// Swap elements i and j
|
|
114
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
115
|
+
}
|
|
116
|
+
return array;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Collects an object's own enumerable values into an array.
|
|
120
|
+
* @param obj - Source object. `null`/`undefined` yields an empty array.
|
|
121
|
+
* @returns Array of the object's values.
|
|
122
|
+
*/
|
|
123
|
+
function objectValueToArray(obj) {
|
|
124
|
+
var arr = [];
|
|
125
|
+
for (var i in obj)
|
|
126
|
+
arr.push(obj[i]);
|
|
127
|
+
return arr;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Groups list items by the key returned from `fn`, similar to Lodash's
|
|
131
|
+
* `groupBy`. Keys are compared by their JSON representation, so they can be
|
|
132
|
+
* primitives or plain objects.
|
|
133
|
+
*
|
|
134
|
+
* @param list - Array-like list of items to group. `null`/`undefined` yields an empty array.
|
|
135
|
+
* @param fn - Maps an item to the value used to group it.
|
|
136
|
+
* @returns Array of groups, each an array of items sharing the same key.
|
|
137
|
+
*/
|
|
138
|
+
function groupBy(list, fn) {
|
|
139
|
+
if (!list)
|
|
140
|
+
return [];
|
|
141
|
+
var groups = {};
|
|
142
|
+
for (var i = 0; i < list.length; i++) {
|
|
143
|
+
var group = JSON.stringify(fn(list[i]));
|
|
144
|
+
if (group in groups) {
|
|
145
|
+
groups[group].push(list[i]);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
groups[group] = [list[i]];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return objectValueToArray(groups);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Remove keys from object or array
|
|
155
|
+
* @param objectOrArray Object or array
|
|
156
|
+
* @param keys Keys to remove
|
|
157
|
+
* @returns Object or array with keys removed
|
|
158
|
+
*/
|
|
159
|
+
function removeByKey(objectOrArray, keys) {
|
|
160
|
+
if (Array.isArray(objectOrArray)) {
|
|
161
|
+
return objectOrArray.map((item) => typeof item === "object" && item !== null ? removeByKey(item, keys) : item);
|
|
162
|
+
}
|
|
163
|
+
if (typeof objectOrArray === "object" && objectOrArray !== null) {
|
|
164
|
+
const entries = Object.entries(objectOrArray)
|
|
165
|
+
.filter(([key]) => !keys.includes(key))
|
|
166
|
+
.map(([key, value]) => {
|
|
167
|
+
if (Array.isArray(value) || (typeof value === "object" && value !== null)) {
|
|
168
|
+
return [key, removeByKey(value, keys)];
|
|
169
|
+
}
|
|
170
|
+
return [key, value];
|
|
171
|
+
});
|
|
172
|
+
return Object.fromEntries(entries);
|
|
173
|
+
}
|
|
174
|
+
// Primitives are returned as-is
|
|
175
|
+
return objectOrArray;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Remove empty values from object or array
|
|
179
|
+
* @param objectOrArray Object or array
|
|
180
|
+
* @param options Options:
|
|
181
|
+
* - removeNull: Remove null values. Default: true
|
|
182
|
+
* - removeUndefined: Remove undefined values. Default: true
|
|
183
|
+
* - removeEmptyString: Remove empty string values. Default: true
|
|
184
|
+
* @returns Object or array with empty values removed
|
|
185
|
+
*/
|
|
186
|
+
function removeEmptyValue(objectOrArray, options = {}) {
|
|
187
|
+
const { removeNull = true, removeUndefined = true, removeEmptyString = true } = options;
|
|
188
|
+
const shouldRemove = (value) => (removeNull && value === null) ||
|
|
189
|
+
(removeUndefined && value === undefined) ||
|
|
190
|
+
(removeEmptyString && value === "");
|
|
191
|
+
if (Array.isArray(objectOrArray)) {
|
|
192
|
+
const result = objectOrArray
|
|
193
|
+
.map((item) => {
|
|
194
|
+
if (Array.isArray(item) || (typeof item === "object" && item !== null)) {
|
|
195
|
+
return removeEmptyValue(item, {
|
|
196
|
+
removeNull,
|
|
197
|
+
removeUndefined,
|
|
198
|
+
removeEmptyString,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return item;
|
|
202
|
+
})
|
|
203
|
+
.filter((item) => !shouldRemove(item));
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
if (typeof objectOrArray === "object" && objectOrArray !== null) {
|
|
207
|
+
const entries = Object.entries(objectOrArray)
|
|
208
|
+
.map(([key, value]) => {
|
|
209
|
+
if (Array.isArray(value) || (typeof value === "object" && value !== null)) {
|
|
210
|
+
return [
|
|
211
|
+
key,
|
|
212
|
+
removeEmptyValue(value, {
|
|
213
|
+
removeNull,
|
|
214
|
+
removeUndefined,
|
|
215
|
+
removeEmptyString,
|
|
216
|
+
}),
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
return [key, value];
|
|
220
|
+
})
|
|
221
|
+
.filter(([, value]) => !shouldRemove(value));
|
|
222
|
+
return Object.fromEntries(entries);
|
|
223
|
+
}
|
|
224
|
+
return objectOrArray;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Generates a random string of the given length, optionally guaranteeing at
|
|
228
|
+
* least one character from each requested category ("upper", "lower",
|
|
229
|
+
* "number", "specific").
|
|
230
|
+
*
|
|
231
|
+
* @param length - Desired length of the resulting string.
|
|
232
|
+
* @param opt - Character categories that must each appear at least once.
|
|
233
|
+
* `"specific"` only counts as a required category when `specificChars` is
|
|
234
|
+
* non-empty; otherwise it's silently ignored (as if not requested at all).
|
|
235
|
+
* @param specificChars - Custom character set used for the `"specific"` category.
|
|
236
|
+
* @returns A random string of exactly `length` characters. If `opt` is empty
|
|
237
|
+
* (or every requested category ends up unusable), falls back to alphanumeric
|
|
238
|
+
* characters rather than returning an empty string.
|
|
239
|
+
* @throws {Error} If `length` is smaller than the number of required categories,
|
|
240
|
+
* since the "at least one of each" guarantee couldn't fit otherwise.
|
|
241
|
+
*/
|
|
242
|
+
function randomString(length, opt = [], specificChars = "") {
|
|
243
|
+
const upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
244
|
+
const lowerChars = "abcdefghijklmnopqrstuvwxyz";
|
|
245
|
+
const numbers = "0123456789";
|
|
246
|
+
const requiredCategories = new Set(opt);
|
|
247
|
+
// Without specificChars, "specific" can't be honored, so treat it as unset
|
|
248
|
+
if (!specificChars) {
|
|
249
|
+
requiredCategories.delete("specific");
|
|
250
|
+
}
|
|
251
|
+
if (length < requiredCategories.size) {
|
|
252
|
+
throw new Error(`length (${length}) is too small to include one of each required category: ${[...requiredCategories].join(", ")}`);
|
|
253
|
+
}
|
|
254
|
+
let result = [];
|
|
255
|
+
let allChars = "";
|
|
256
|
+
const randomOneChar = (chars) => chars.charAt(Math.floor(Math.random() * chars.length));
|
|
257
|
+
if (requiredCategories.has("upper")) {
|
|
258
|
+
allChars += upperChars;
|
|
259
|
+
// Guarantee at least one uppercase letter
|
|
260
|
+
result.push(randomOneChar(upperChars));
|
|
261
|
+
}
|
|
262
|
+
if (requiredCategories.has("lower")) {
|
|
263
|
+
allChars += lowerChars;
|
|
264
|
+
// Guarantee at least one lowercase letter
|
|
265
|
+
result.push(randomOneChar(lowerChars));
|
|
266
|
+
}
|
|
267
|
+
if (requiredCategories.has("number")) {
|
|
268
|
+
allChars += numbers;
|
|
269
|
+
// Guarantee at least one digit
|
|
270
|
+
result.push(randomOneChar(numbers));
|
|
271
|
+
}
|
|
272
|
+
if (requiredCategories.has("specific")) {
|
|
273
|
+
allChars += specificChars;
|
|
274
|
+
// Guarantee at least one character from the custom set
|
|
275
|
+
result.push(randomOneChar(specificChars));
|
|
276
|
+
}
|
|
277
|
+
// If no category applied (e.g. opt is empty, or only "specific" was
|
|
278
|
+
// requested without specificChars), fall back to alphanumeric so the
|
|
279
|
+
// result isn't silently empty.
|
|
280
|
+
if (!allChars) {
|
|
281
|
+
allChars = upperChars + lowerChars + numbers;
|
|
282
|
+
}
|
|
283
|
+
// Fill up the remaining characters to reach the requested length
|
|
284
|
+
for (let i = result.length; i < length; i++) {
|
|
285
|
+
result.push(randomOneChar(allChars));
|
|
286
|
+
}
|
|
287
|
+
// Shuffle so the guaranteed characters aren't always at the front
|
|
288
|
+
return shuffleArray(result).join("");
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Formats a byte value (0-255) as a zero-padded, 2-digit lowercase hex string.
|
|
292
|
+
* Intended for internal use with well-formed byte values (e.g. from a
|
|
293
|
+
* `Uint8Array`); values outside 0-255 are not validated.
|
|
294
|
+
*
|
|
295
|
+
* @param b - Byte value, expected in the 0-255 range.
|
|
296
|
+
* @returns 2-character hex string, e.g. `"0f"`.
|
|
297
|
+
*/
|
|
298
|
+
function byte2hex(b) {
|
|
299
|
+
return ("0" + b.toString(16)).slice(-2);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Generates the raw 16 bytes of a UUIDv7 (timestamp + random, RFC 4122
|
|
303
|
+
* variant). Relies on the Web Crypto `crypto.getRandomValues` API, available
|
|
304
|
+
* in browsers and modern Node.js.
|
|
305
|
+
*
|
|
306
|
+
* @returns 16-byte `Uint8Array` encoding a UUIDv7.
|
|
307
|
+
*/
|
|
308
|
+
function uuid7bin() {
|
|
309
|
+
var bytes = new Uint8Array(16);
|
|
310
|
+
var now = Date.now();
|
|
311
|
+
// 48-bit timestamp (ms). Division (rather than a left shift) is used so
|
|
312
|
+
// values above the 32-bit range bitwise operators support are still split
|
|
313
|
+
// correctly; `&` truncates the fractional part left over from the division.
|
|
314
|
+
bytes[0] = (now / 0x10000000000) & 0xff;
|
|
315
|
+
bytes[1] = (now / 0x100000000) & 0xff;
|
|
316
|
+
bytes[2] = (now / 0x1000000) & 0xff;
|
|
317
|
+
bytes[3] = (now / 0x10000) & 0xff;
|
|
318
|
+
bytes[4] = (now / 0x100) & 0xff;
|
|
319
|
+
bytes[5] = now & 0xff;
|
|
320
|
+
// random 10 bytes
|
|
321
|
+
var rnd = new Uint8Array(10);
|
|
322
|
+
crypto.getRandomValues(rnd);
|
|
323
|
+
for (var i = 0; i < 10; i++) {
|
|
324
|
+
bytes[i + 6] = rnd[i];
|
|
325
|
+
}
|
|
326
|
+
// version 7
|
|
327
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
|
328
|
+
// variant RFC4122
|
|
329
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
330
|
+
return bytes;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Generates a UUIDv7 string (e.g. `"01890a5d-ac96-774b-bcce-b302099a8057"`).
|
|
334
|
+
* @returns UUIDv7 in the standard 8-4-4-4-12 hex string format.
|
|
335
|
+
*/
|
|
336
|
+
function uuid7() {
|
|
337
|
+
var bin = uuid7bin();
|
|
338
|
+
var hex = [];
|
|
339
|
+
for (var i = 0; i < bin.length; i++) {
|
|
340
|
+
hex.push(byte2hex(bin[i]));
|
|
341
|
+
}
|
|
342
|
+
return [
|
|
343
|
+
hex.slice(0, 4).join(""),
|
|
344
|
+
hex.slice(4, 6).join(""),
|
|
345
|
+
hex.slice(6, 8).join(""),
|
|
346
|
+
hex.slice(8, 10).join(""),
|
|
347
|
+
hex.slice(10).join(""),
|
|
348
|
+
].join("-");
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Encodes a string to base64 (UTF-8 bytes).
|
|
352
|
+
* @param str - String to encode.
|
|
353
|
+
* @returns Base64-encoded string.
|
|
354
|
+
*/
|
|
355
|
+
function base64encode(str) {
|
|
356
|
+
const bytes = new TextEncoder().encode(str);
|
|
357
|
+
// Spreading the whole byte array into String.fromCharCode blows the call
|
|
358
|
+
// stack for large inputs, so build the binary string in bounded chunks.
|
|
359
|
+
const chunkSize = 0x8000;
|
|
360
|
+
let binary = "";
|
|
361
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
362
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
|
|
363
|
+
}
|
|
364
|
+
return btoa(binary);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Decodes a base64 string back to its original (UTF-8) string.
|
|
368
|
+
* @param base64 - Base64-encoded string.
|
|
369
|
+
* @returns Decoded string.
|
|
370
|
+
*/
|
|
371
|
+
function base64decode(base64) {
|
|
372
|
+
return new TextDecoder().decode(Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)));
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Reads a nested property from an object using a dot-separated path.
|
|
376
|
+
* Example: `getObjectValue(user, 'abc.def')` returns `user.abc.def`.
|
|
377
|
+
*
|
|
378
|
+
* Equivalent to optional chaining (`object?.abc?.def`), for use on
|
|
379
|
+
* TypeScript versions below 3.7 (e.g. Angular versions below 9). On newer
|
|
380
|
+
* TypeScript versions, prefer optional chaining directly instead.
|
|
381
|
+
*
|
|
382
|
+
* @param object - Source object.
|
|
383
|
+
* @param path - Dot-separated property path.
|
|
384
|
+
* @returns The resolved value, or `null`/`undefined` if any segment is missing.
|
|
385
|
+
* @global
|
|
386
|
+
*/
|
|
387
|
+
function getObjectValue(object, path) {
|
|
388
|
+
if (!object)
|
|
389
|
+
return null;
|
|
390
|
+
return path
|
|
391
|
+
.split(".")
|
|
392
|
+
.reduce((acc, part) => acc && acc[part], object);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Short alias for {@link getObjectValue}.
|
|
396
|
+
* @param object - Source object.
|
|
397
|
+
* @param path - Dot-separated property path.
|
|
398
|
+
* @returns The resolved value, or `null`/`undefined` if any segment is missing.
|
|
399
|
+
*/
|
|
400
|
+
function ov(object, path) {
|
|
401
|
+
return getObjectValue(object, path);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Converts a value to an integer, similar to `parseInt`/`Number` but with an
|
|
405
|
+
* explicit fallback for empty values (see {@link isEmpty}).
|
|
406
|
+
*
|
|
407
|
+
* @param value - Value to convert.
|
|
408
|
+
* @param defaultValue - Value returned when `value` is empty. Defaults to `0`.
|
|
409
|
+
* @returns The parsed integer, `defaultValue` if `value` is empty, or `NaN`
|
|
410
|
+
* if `value` is a non-numeric, non-empty string.
|
|
411
|
+
*/
|
|
412
|
+
function toInt(value, defaultValue = 0) {
|
|
413
|
+
if (isEmpty(value))
|
|
414
|
+
return defaultValue;
|
|
415
|
+
return typeof value === "string" ? Number.parseInt(value.trim()) : Number(value);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Converts a positive integer to a Roman numeral. Only supports the standard
|
|
419
|
+
* range (1-3999); larger numbers produce a non-standard repeating "M" prefix.
|
|
420
|
+
*
|
|
421
|
+
* @param num - Number to convert.
|
|
422
|
+
* @returns Roman numeral string, or `""` if `num` is zero or negative.
|
|
423
|
+
*/
|
|
424
|
+
function numberToRoman(num) {
|
|
425
|
+
if (num <= 0)
|
|
426
|
+
return "";
|
|
427
|
+
const romanMap = [
|
|
428
|
+
{ value: 1000, numeral: "M" },
|
|
429
|
+
{ value: 900, numeral: "CM" },
|
|
430
|
+
{ value: 500, numeral: "D" },
|
|
431
|
+
{ value: 400, numeral: "CD" },
|
|
432
|
+
{ value: 100, numeral: "C" },
|
|
433
|
+
{ value: 90, numeral: "XC" },
|
|
434
|
+
{ value: 50, numeral: "L" },
|
|
435
|
+
{ value: 40, numeral: "XL" },
|
|
436
|
+
{ value: 10, numeral: "X" },
|
|
437
|
+
{ value: 9, numeral: "IX" },
|
|
438
|
+
{ value: 5, numeral: "V" },
|
|
439
|
+
{ value: 4, numeral: "IV" },
|
|
440
|
+
{ value: 1, numeral: "I" },
|
|
441
|
+
];
|
|
442
|
+
let result = "";
|
|
443
|
+
for (const item of romanMap) {
|
|
444
|
+
while (num >= item.value) {
|
|
445
|
+
result += item.numeral;
|
|
446
|
+
num -= item.value;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Converts a Roman numeral string to a number. Does not validate that the
|
|
453
|
+
* input is a well-formed Roman numeral (use {@link isRomanNumber} first if
|
|
454
|
+
* that matters) — unrecognized characters make the result `NaN`.
|
|
455
|
+
*
|
|
456
|
+
* @param roman - Roman numeral string (case-sensitive, uppercase letters).
|
|
457
|
+
* @returns The numeric value, or `NaN` if `roman` contains unrecognized characters.
|
|
458
|
+
*/
|
|
459
|
+
function romanToNumber(roman) {
|
|
460
|
+
const map = {
|
|
461
|
+
I: 1,
|
|
462
|
+
V: 5,
|
|
463
|
+
X: 10,
|
|
464
|
+
L: 50,
|
|
465
|
+
C: 100,
|
|
466
|
+
D: 500,
|
|
467
|
+
M: 1000,
|
|
468
|
+
};
|
|
469
|
+
let result = 0;
|
|
470
|
+
for (let i = 0; i < roman.length; i++) {
|
|
471
|
+
const current = map[roman.charAt(i)];
|
|
472
|
+
const next = map[roman.charAt(i + 1)];
|
|
473
|
+
if (next && current < next) {
|
|
474
|
+
result -= current;
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
result += current;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Checks whether a string is a well-formed Roman numeral (1-3999), matching
|
|
484
|
+
* the standard subtractive notation. Case-insensitive.
|
|
485
|
+
*
|
|
486
|
+
* @param value - String to validate.
|
|
487
|
+
* @returns `true` if `value` is a valid Roman numeral.
|
|
488
|
+
*/
|
|
489
|
+
function isRomanNumber(value) {
|
|
490
|
+
if (!value)
|
|
491
|
+
return false;
|
|
492
|
+
const romanRegex = /^(M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3}))$/;
|
|
493
|
+
return romanRegex.test(value.toUpperCase());
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Converts a base64 string to a `Blob`. Browser-only (relies on `atob` and
|
|
497
|
+
* `Blob`).
|
|
498
|
+
*
|
|
499
|
+
* @param base64 - Base64-encoded content.
|
|
500
|
+
* @param mimeType - MIME type to assign to the resulting `Blob`.
|
|
501
|
+
* @param sliceSize - Chunk size (in decoded characters) used while building
|
|
502
|
+
* the byte arrays, to avoid excessive memory allocation for large inputs.
|
|
503
|
+
* @returns A `Blob` containing the decoded bytes.
|
|
504
|
+
*/
|
|
505
|
+
function base64ToBlob(base64, mimeType, sliceSize = 512) {
|
|
506
|
+
const byteCharacters = atob(base64);
|
|
507
|
+
const bytes = [];
|
|
508
|
+
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
|
509
|
+
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
|
510
|
+
const byteNumbers = new Array(slice.length);
|
|
511
|
+
for (let i = 0; i < slice.length; i++) {
|
|
512
|
+
byteNumbers[i] = slice.charCodeAt(i);
|
|
513
|
+
}
|
|
514
|
+
const byte = new Uint8Array(byteNumbers);
|
|
515
|
+
bytes.push(byte);
|
|
516
|
+
}
|
|
517
|
+
return new Blob(bytes, { type: mimeType });
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Triggers a browser download (or opens in a tab) for base64-encoded file
|
|
521
|
+
* content. Browser-only (relies on `document`, `URL.createObjectURL`).
|
|
522
|
+
*
|
|
523
|
+
* @param fileName - Suggested file name for the download.
|
|
524
|
+
* @param mimeType - MIME type/subtype (a bare subtype like `"pdf"` is
|
|
525
|
+
* expanded to `"application/pdf"`).
|
|
526
|
+
* @param base64Content - Base64-encoded file content.
|
|
527
|
+
* @param action - `"download"` saves the file, `"open"` opens it in the same
|
|
528
|
+
* tab, `"open_blank"` opens it in a new tab. Defaults to `"download"`.
|
|
529
|
+
*/
|
|
530
|
+
function downloadFile(fileName, mimeType, base64Content, action = "download") {
|
|
531
|
+
if (!mimeType.startsWith("application/")) {
|
|
532
|
+
mimeType = `application/${mimeType}`;
|
|
533
|
+
}
|
|
534
|
+
const blob = base64ToBlob(base64Content, mimeType);
|
|
535
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
536
|
+
let link = document.createElement("a");
|
|
537
|
+
document.body.appendChild(link);
|
|
538
|
+
link.download = fileName;
|
|
539
|
+
if (action === "open_blank") {
|
|
540
|
+
link.target = "_blank";
|
|
541
|
+
}
|
|
542
|
+
else if (action === "open") {
|
|
543
|
+
link.target = "_self";
|
|
544
|
+
}
|
|
545
|
+
link.href = blobUrl;
|
|
546
|
+
link.click();
|
|
547
|
+
document.body.removeChild(link);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Recursively clones plain objects, arrays and `Date` instances.
|
|
551
|
+
*
|
|
552
|
+
* Note: `Date` has no own enumerable properties, so a plain for-in copy
|
|
553
|
+
* would silently turn every `Date` into an empty `{}` — it's special-cased
|
|
554
|
+
* here. Other special object types (`Map`, `Set`, `RegExp`, ...) are not
|
|
555
|
+
* handled and will also be cloned as plain `{}`.
|
|
556
|
+
*
|
|
557
|
+
* @param obj - Value to clone.
|
|
558
|
+
* @returns A deep copy of `obj` (primitives are returned as-is).
|
|
559
|
+
*/
|
|
560
|
+
function deepClone(obj) {
|
|
561
|
+
if (obj === null || typeof obj !== "object") {
|
|
562
|
+
return obj;
|
|
563
|
+
}
|
|
564
|
+
if (obj instanceof Date) {
|
|
565
|
+
return new Date(obj.getTime());
|
|
566
|
+
}
|
|
567
|
+
let clonedObj = Array.isArray(obj) ? [] : {};
|
|
568
|
+
for (let key in obj) {
|
|
569
|
+
if (obj.hasOwnProperty(key)) {
|
|
570
|
+
clonedObj[key] = deepClone(obj[key]);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return clonedObj;
|
|
574
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./base-enum"), exports);
|
|
18
|
+
__exportStar(require("./func"), exports);
|
|
19
|
+
__exportStar(require("./number-to-words"), exports);
|
|
20
|
+
__exportStar(require("./round"), exports);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NumberToWordsDefinition } from "./types";
|
|
2
|
+
/** Spells out a non-negative integer using the given locale's rules. */
|
|
3
|
+
export declare function convertIntegerPart(value: number, definition: NumberToWordsDefinition, num: number | string, locale: string): string;
|
|
4
|
+
/** Spells out a fractional part one digit at a time, e.g. "05" -> "không năm". */
|
|
5
|
+
export declare function convertFractionalPart(fractionalDigits: string, definition: NumberToWordsDefinition): string;
|
|
6
|
+
/**
|
|
7
|
+
* Validates `num` and normalizes it to a plain (non-exponential, unsigned)
|
|
8
|
+
* numeric string with a `-` prefix kept only to signal the sign, e.g.
|
|
9
|
+
* `123` -> `"123"`, `"-1.50"` -> `"-1.50"`.
|
|
10
|
+
*
|
|
11
|
+
* A `number` is only accepted when it's an integer — a non-integer `number`
|
|
12
|
+
* has already gone through IEEE-754 float conversion by the time this
|
|
13
|
+
* function sees it (trailing zeros lost, rounding artifacts like
|
|
14
|
+
* `0.1 + 0.2`), so decimals must be passed as a string instead, which is
|
|
15
|
+
* read back digit-for-digit exactly as written.
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizeNumericInput(num: number | string): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.convertIntegerPart = convertIntegerPart;
|
|
4
|
+
exports.convertFractionalPart = convertFractionalPart;
|
|
5
|
+
exports.normalizeNumericInput = normalizeNumericInput;
|
|
6
|
+
/** Splits a non-negative integer into groups of 3 digits, most significant first. */
|
|
7
|
+
function splitIntoGroups(num) {
|
|
8
|
+
if (num === 0)
|
|
9
|
+
return [0];
|
|
10
|
+
const groups = [];
|
|
11
|
+
let n = num;
|
|
12
|
+
while (n > 0) {
|
|
13
|
+
groups.unshift(n % 1000);
|
|
14
|
+
n = Math.floor(n / 1000);
|
|
15
|
+
}
|
|
16
|
+
return groups;
|
|
17
|
+
}
|
|
18
|
+
/** Spells out a non-negative integer using the given locale's rules. */
|
|
19
|
+
function convertIntegerPart(value, definition, num, locale) {
|
|
20
|
+
if (value === 0)
|
|
21
|
+
return definition.zero;
|
|
22
|
+
const groups = splitIntoGroups(value);
|
|
23
|
+
const maxScaleIndex = groups.length - 1;
|
|
24
|
+
if (maxScaleIndex >= definition.scaleWords.length) {
|
|
25
|
+
throw new Error(`numberToWords: ${num} exceeds the supported range for locale "${locale}"`);
|
|
26
|
+
}
|
|
27
|
+
const segments = [];
|
|
28
|
+
let isLeadingGroup = true;
|
|
29
|
+
groups.forEach((group, i) => {
|
|
30
|
+
if (group === 0)
|
|
31
|
+
return;
|
|
32
|
+
const scaleIndex = maxScaleIndex - i;
|
|
33
|
+
const scaleWord = definition.scaleWords[scaleIndex];
|
|
34
|
+
const groupWords = definition.convertGroup(group, isLeadingGroup);
|
|
35
|
+
segments.push(scaleWord ? `${groupWords} ${scaleWord}` : groupWords);
|
|
36
|
+
isLeadingGroup = false;
|
|
37
|
+
});
|
|
38
|
+
return segments.join(" ");
|
|
39
|
+
}
|
|
40
|
+
/** Spells out a fractional part one digit at a time, e.g. "05" -> "không năm". */
|
|
41
|
+
function convertFractionalPart(fractionalDigits, definition) {
|
|
42
|
+
return [...fractionalDigits].map((d) => definition.digits[Number(d)]).join(" ");
|
|
43
|
+
}
|
|
44
|
+
const numericStringPattern = /^-?\d+(\.\d+)?$/;
|
|
45
|
+
/**
|
|
46
|
+
* Validates `num` and normalizes it to a plain (non-exponential, unsigned)
|
|
47
|
+
* numeric string with a `-` prefix kept only to signal the sign, e.g.
|
|
48
|
+
* `123` -> `"123"`, `"-1.50"` -> `"-1.50"`.
|
|
49
|
+
*
|
|
50
|
+
* A `number` is only accepted when it's an integer — a non-integer `number`
|
|
51
|
+
* has already gone through IEEE-754 float conversion by the time this
|
|
52
|
+
* function sees it (trailing zeros lost, rounding artifacts like
|
|
53
|
+
* `0.1 + 0.2`), so decimals must be passed as a string instead, which is
|
|
54
|
+
* read back digit-for-digit exactly as written.
|
|
55
|
+
*/
|
|
56
|
+
function normalizeNumericInput(num) {
|
|
57
|
+
if (typeof num === "number") {
|
|
58
|
+
if (!Number.isFinite(num)) {
|
|
59
|
+
throw new Error(`numberToWords: ${num} is not a finite number`);
|
|
60
|
+
}
|
|
61
|
+
if (!Number.isInteger(num)) {
|
|
62
|
+
throw new Error(`numberToWords: ${num} is a non-integer number, which may have lost precision as a float ` +
|
|
63
|
+
`(trailing zeros, rounding) — pass it as a string instead, e.g. "${num}"`);
|
|
64
|
+
}
|
|
65
|
+
return num.toString();
|
|
66
|
+
}
|
|
67
|
+
const trimmed = num.trim();
|
|
68
|
+
if (!numericStringPattern.test(trimmed)) {
|
|
69
|
+
throw new Error(`numberToWords: "${num}" is not a valid numeric string`);
|
|
70
|
+
}
|
|
71
|
+
return trimmed;
|
|
72
|
+
}
|