@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,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,4 @@
1
+ export * from "./base-enum";
2
+ export * from "./func";
3
+ export * from "./number-to-words";
4
+ export * from "./round";
@@ -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
+ }