@ix-xs/node-comfort 1.1.31 → 2.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 (77) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/LICENSE +21 -0
  3. package/README.md +210 -748
  4. package/index.js +84 -13
  5. package/package.json +201 -21
  6. package/src/Arr.js +901 -0
  7. package/src/Async.js +585 -0
  8. package/src/Cache.js +329 -0
  9. package/src/Checker.js +979 -188
  10. package/src/Cli.js +1034 -0
  11. package/src/Color.js +415 -0
  12. package/src/Crypto.js +707 -0
  13. package/src/Emitter.js +289 -0
  14. package/src/Env.js +555 -0
  15. package/src/FS.js +1483 -768
  16. package/src/Func.js +692 -0
  17. package/src/Http.js +538 -0
  18. package/src/Id.js +418 -0
  19. package/src/Logger.js +739 -307
  20. package/src/Num.js +849 -0
  21. package/src/Obj.js +904 -0
  22. package/src/SQLite.js +1308 -994
  23. package/src/Schema.js +1586 -0
  24. package/src/Str.js +999 -0
  25. package/src/Sys.js +511 -0
  26. package/src/Time.js +1323 -0
  27. package/src/Utils.js +321 -325
  28. package/src/errors.js +292 -0
  29. package/types/index.d.ts +693 -120
  30. package/types/src/Arr.d.ts +562 -0
  31. package/types/src/Arr.d.ts.map +1 -0
  32. package/types/src/Async.d.ts +321 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +195 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +609 -81
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +582 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +294 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +534 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +170 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +382 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +709 -201
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +455 -0
  51. package/types/src/Func.d.ts.map +1 -0
  52. package/types/src/Http.d.ts +374 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +200 -0
  55. package/types/src/Id.d.ts.map +1 -0
  56. package/types/src/Logger.d.ts +374 -24
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +578 -0
  59. package/types/src/Num.d.ts.map +1 -0
  60. package/types/src/Obj.d.ts +475 -0
  61. package/types/src/Obj.d.ts.map +1 -0
  62. package/types/src/SQLite.d.ts +647 -305
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +940 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +696 -0
  67. package/types/src/Str.d.ts.map +1 -0
  68. package/types/src/Sys.d.ts +269 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +664 -0
  71. package/types/src/Time.d.ts.map +1 -0
  72. package/types/src/Utils.d.ts +154 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +292 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/tsconfig.json +0 -18
  77. package/types/index.d.ts.map +0 -1
package/src/Num.js ADDED
@@ -0,0 +1,849 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Numbers: clamping, rounding that gets decimals right, statistics, and
5
+ * formatting for humans (sizes, money, ordinals, compact numbers) in any
6
+ * language. Formatting is English by default: change it with `setLocale()`
7
+ * or per call with `{ locale }`.
8
+ *
9
+ * @example
10
+ * num.round(1.005, 2); // 1.01
11
+ * num.formatBytes(1536); // "1.5 KB"
12
+ * num.currency(1234.5, "EUR", { locale: "fr" }); // "1 234,50 €"
13
+ * num.percentile(latencies, 95);
14
+ */
15
+
16
+ /** The default language, changed with `setLocale()`. */
17
+ const _config = { locale: "en" };
18
+
19
+ /**
20
+ * Options for `formatBytes()`.
21
+ * @typedef {object} FormatBytesOptions
22
+ * @property {number} [decimals] Maximum decimals. Defaults to `1`.
23
+ * @property {boolean} [binary] 1 KB is 1024 bytes when `true`, 1000 when `false`. Defaults to `true`.
24
+ * @property {boolean} [iec] Use `KiB`, `MiB`, `GiB` labels.
25
+ * @property {string} [locale] Formats the number for a language, like `"fr"` for `"1,5 KB"`.
26
+ * @property {string[]} [units] Your own labels from bytes up, like `["o", "Ko", "Mo", "Go", "To"]`.
27
+ */
28
+
29
+ /**
30
+ * Options for `parseBytes()`.
31
+ * @typedef {object} ParseBytesOptions
32
+ * @property {boolean} [binary] 1 KB is 1024 bytes when `true`, 1000 when `false`. `KiB` units are always 1024. Defaults to `true`.
33
+ */
34
+
35
+ /**
36
+ * Options for `format()`.
37
+ * @typedef {object} FormatNumberOptions
38
+ * @property {string} [locale] Language, like `"fr"` or `"en-US"`. Defaults to the one set with `setLocale()`, or `"en"`.
39
+ * @property {number} [decimals] Exact number of decimals.
40
+ * @property {number} [minDecimals] Minimum decimals.
41
+ * @property {number} [maxDecimals] Maximum decimals. Defaults to `3`.
42
+ * @property {boolean} [compact] Short notation like `1.2K` or `3.4M`.
43
+ * @property {"percent"|"decimal"} [style] `"percent"` multiplies by 100 and adds `%`.
44
+ * @property {string} [unit] Any `Intl` unit: `"kilometer"`, `"celsius"`, `"megabyte"`...
45
+ * @property {"short"|"long"|"narrow"} [unitDisplay] How the unit is written. Defaults to `"short"`.
46
+ * @property {"auto"|"always"|"never"|"exceptZero"} [signDisplay] When to show the sign. `"exceptZero"` gives `+5`.
47
+ * @property {boolean} [grouping] Thousands separators. Defaults to `true`.
48
+ */
49
+
50
+ /**
51
+ * Options for `currency()`.
52
+ * @typedef {object} CurrencyOptions
53
+ * @property {string} [locale] Language. Defaults to the one set with `setLocale()`, or `"en"`.
54
+ * @property {number} [decimals] Defaults to the currency's usual decimals (2 for EUR, 0 for JPY).
55
+ * @property {"symbol"|"narrowSymbol"|"code"|"name"} [display] `€`, `€`, `EUR` or `euros`. Defaults to `"symbol"`.
56
+ * @property {boolean} [accounting] Show negative amounts in parentheses, like `($5.00)`.
57
+ */
58
+
59
+ /**
60
+ * Options for `ordinal()`.
61
+ * @typedef {object} OrdinalOptions
62
+ * @property {string} [locale] Built in: `en`, `fr`, `es`, `it`, `pt`, `de`, `nl`. Defaults to the one set with `setLocale()`, or `"en"`.
63
+ * @property {Partial<Record<Intl.LDMLPluralRule, string>>} [suffixes] Your own suffixes, for other languages.
64
+ */
65
+
66
+ /**
67
+ * Options for `variance()` and `stdDev()`.
68
+ * @typedef {object} VarianceOptions
69
+ * @property {boolean} [sample] Sample statistic (divides by n - 1) instead of population.
70
+ */
71
+
72
+ /**
73
+ * Multiplies by 10^exp without floating-point error.
74
+ * @param {number} value
75
+ * @param {number} exp
76
+ * @returns {number}
77
+ */
78
+ const _shift = (value, exp) => {
79
+ const [mantissa, e = "0"] = String(value).split("e");
80
+ return Number(`${mantissa}e${Number(e) + exp}`);
81
+ };
82
+
83
+ /**
84
+ * @param {number} value
85
+ * @param {number} decimals
86
+ * @param {(n: number) => number} fn
87
+ * @returns {number}
88
+ */
89
+ const _roundWith = (value, decimals, fn) => {
90
+ if (!Number.isFinite(value)) return value;
91
+ return _shift(fn(_shift(value, decimals)), -decimals);
92
+ };
93
+
94
+ const _SIZE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
95
+ const _IEC_UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
96
+
97
+ /** @type {Record<string, Partial<Record<Intl.LDMLPluralRule, string>>>} */
98
+ const _ORDINAL_SUFFIXES = {
99
+ en: { one: "st", two: "nd", few: "rd", other: "th" },
100
+ fr: { one: "er", other: "e" },
101
+ es: { other: "º" },
102
+ it: { other: "º" },
103
+ pt: { other: "º" },
104
+ de: { other: "." },
105
+ nl: { other: "e" },
106
+ };
107
+
108
+ /**
109
+ * Keeps a number between `min` and `max`.
110
+ *
111
+ * @example
112
+ * num.clamp(150, 0, 100); // 100
113
+ * num.clamp(-5, 0, 100); // 0
114
+ *
115
+ * @param {number} value
116
+ * @param {number} min
117
+ * @param {number} max
118
+ * @returns {number}
119
+ */
120
+ function clamp(value, min, max) {
121
+ return Math.min(Math.max(value, min), max);
122
+ }
123
+
124
+ /**
125
+ * Is the number between `min` and `max`, both included?
126
+ *
127
+ * @example
128
+ * num.inRange(10, 0, 10); // true
129
+ *
130
+ * @param {number} value
131
+ * @param {number} min
132
+ * @param {number} max
133
+ * @returns {boolean}
134
+ */
135
+ function inRange(value, min, max) {
136
+ return value >= min && value <= max;
137
+ }
138
+
139
+ /**
140
+ * Wraps a number around a range, like an angle or a carousel index.
141
+ * `max` is excluded.
142
+ *
143
+ * @example
144
+ * num.wrap(370, 0, 360); // 10
145
+ * num.wrap(-1, 0, 5); // 4
146
+ *
147
+ * @param {number} value
148
+ * @param {number} min
149
+ * @param {number} max
150
+ * @returns {number}
151
+ */
152
+ function wrap(value, min, max) {
153
+ const span = max - min;
154
+ if (span <= 0) return min;
155
+ return ((((value - min) % span) + span) % span) + min;
156
+ }
157
+
158
+ /**
159
+ * Rounds to `decimals` places without floating-point surprises:
160
+ * `Math.round(1.005 * 100) / 100` gives `1`, this gives `1.01`. Halves round
161
+ * away from zero, like in a spreadsheet.
162
+ *
163
+ * @example
164
+ * num.round(1.005, 2); // 1.01
165
+ * num.round(1234, -2); // 1200
166
+ * num.round(-2.5); // -3
167
+ *
168
+ * @param {number} value
169
+ * @param {number} [decimals=0] Negative values round to tens, hundreds...
170
+ * @returns {number}
171
+ */
172
+ function round(value, decimals = 0) {
173
+ return _roundWith(value, decimals, (n) => Math.sign(n) * Math.round(Math.abs(n)) || 0);
174
+ }
175
+
176
+ /**
177
+ * Rounds down to `decimals` places.
178
+ *
179
+ * @example
180
+ * num.floor(1.789, 2); // 1.78
181
+ *
182
+ * @param {number} value
183
+ * @param {number} [decimals=0]
184
+ * @returns {number}
185
+ */
186
+ function floor(value, decimals = 0) {
187
+ return _roundWith(value, decimals, Math.floor);
188
+ }
189
+
190
+ /**
191
+ * Rounds up to `decimals` places.
192
+ *
193
+ * @example
194
+ * num.ceil(1.231, 2); // 1.24
195
+ *
196
+ * @param {number} value
197
+ * @param {number} [decimals=0]
198
+ * @returns {number}
199
+ */
200
+ function ceil(value, decimals = 0) {
201
+ return _roundWith(value, decimals, Math.ceil);
202
+ }
203
+
204
+ /**
205
+ * Rounds to the nearest multiple of `step`.
206
+ *
207
+ * @example
208
+ * num.snap(17, 5); // 15
209
+ * num.snap(0.37, 0.25); // 0.25
210
+ *
211
+ * @param {number} value
212
+ * @param {number} step
213
+ * @returns {number}
214
+ */
215
+ function snap(value, step) {
216
+ if (!step) return value;
217
+ const decimals = (String(step).split(".")[1] ?? "").length;
218
+ return round(Math.round(value / step) * step, decimals);
219
+ }
220
+
221
+ /**
222
+ * Are two numbers equal within a small tolerance? The right way to compare
223
+ * float results, since `0.1 + 0.2 === 0.3` is `false`.
224
+ *
225
+ * @example
226
+ * num.approxEqual(0.1 + 0.2, 0.3); // true
227
+ *
228
+ * @param {number} a
229
+ * @param {number} b
230
+ * @param {number} [epsilon=Number.EPSILON * 16] Allowed difference, relative to the size of the numbers.
231
+ * @returns {boolean}
232
+ */
233
+ function approxEqual(a, b, epsilon = Number.EPSILON * 16) {
234
+ if (a === b) return true;
235
+ const diff = Math.abs(a - b);
236
+ return diff <= epsilon * Math.max(1, Math.abs(a), Math.abs(b));
237
+ }
238
+
239
+ /**
240
+ * The value at ratio `t` between `start` and `end`.
241
+ *
242
+ * @example
243
+ * num.lerp(0, 100, 0.25); // 25
244
+ *
245
+ * @param {number} start
246
+ * @param {number} end
247
+ * @param {number} t Usually between 0 and 1.
248
+ * @returns {number}
249
+ */
250
+ function lerp(start, end, t) {
251
+ return start + (end - start) * t;
252
+ }
253
+
254
+ /**
255
+ * Moves a number from one range to another, keeping its relative position.
256
+ *
257
+ * @example
258
+ * num.mapRange(5, 0, 10, 0, 100); // 50
259
+ * num.mapRange(75, 0, 100, 1, 0); // 0.25
260
+ *
261
+ * @param {number} value
262
+ * @param {number} inMin
263
+ * @param {number} inMax
264
+ * @param {number} outMin
265
+ * @param {number} outMax
266
+ * @returns {number}
267
+ */
268
+ function mapRange(value, inMin, inMax, outMin, outMax) {
269
+ if (inMax === inMin) return outMin;
270
+ return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
271
+ }
272
+
273
+ /**
274
+ * A random float from `min` (included) to `max` (excluded). Not for
275
+ * security: it uses `Math.random`.
276
+ *
277
+ * @example
278
+ * num.random(10, 20); // 13.84...
279
+ *
280
+ * @param {number} [min=0]
281
+ * @param {number} [max=1]
282
+ * @returns {number}
283
+ */
284
+ function random(min = 0, max = 1) {
285
+ return Math.random() * (max - min) + min;
286
+ }
287
+
288
+ /**
289
+ * A random integer from `min` to `max`, both included. Not for security.
290
+ *
291
+ * @example
292
+ * num.randomInt(1, 6); // a dice roll
293
+ *
294
+ * @param {number} min
295
+ * @param {number} max
296
+ * @returns {number}
297
+ */
298
+ function randomInt(min, max) {
299
+ const lo = Math.ceil(Math.min(min, max));
300
+ const hi = Math.floor(Math.max(min, max));
301
+ return Math.floor(Math.random() * (hi - lo + 1)) + lo;
302
+ }
303
+
304
+ /**
305
+ * Adds up numbers.
306
+ *
307
+ * @example
308
+ * num.sum([1, 2, 3]); // 6
309
+ *
310
+ * @param {Iterable<number>} values
311
+ * @returns {number}
312
+ */
313
+ function sum(values) {
314
+ let total = 0;
315
+ for (const n of values) total += n;
316
+ return total;
317
+ }
318
+
319
+ /**
320
+ * The mean, or `0` for an empty list.
321
+ *
322
+ * @example
323
+ * num.average([2, 4, 9]); // 5
324
+ *
325
+ * @param {Iterable<number>} values
326
+ * @returns {number}
327
+ */
328
+ function average(values) {
329
+ const list = [...values];
330
+ return list.length ? sum(list) / list.length : 0;
331
+ }
332
+
333
+ /**
334
+ * The middle value once sorted, or `0` for an empty list.
335
+ *
336
+ * @example
337
+ * num.median([3, 1, 2]); // 2
338
+ * num.median([1, 2, 3, 4]); // 2.5
339
+ *
340
+ * @param {Iterable<number>} values
341
+ * @returns {number}
342
+ */
343
+ function median(values) {
344
+ const sorted = [...values].sort((a, b) => a - b);
345
+ if (!sorted.length) return 0;
346
+ const mid = Math.floor(sorted.length / 2);
347
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
348
+ }
349
+
350
+ /**
351
+ * The most frequent values.
352
+ *
353
+ * @example
354
+ * num.mode([1, 1, 2, 2, 3]); // [1, 2]
355
+ *
356
+ * @param {Iterable<number>} values
357
+ * @returns {number[]}
358
+ */
359
+ function mode(values) {
360
+ /** @type {Map<number, number>} */
361
+ const counts = new Map();
362
+ for (const n of values) counts.set(n, (counts.get(n) ?? 0) + 1);
363
+ const best = Math.max(0, ...counts.values());
364
+ return [...counts].filter(([, c]) => c === best && best > 0).map(([n]) => n);
365
+ }
366
+
367
+ /**
368
+ * The variance: the average squared distance from the mean.
369
+ *
370
+ * @example
371
+ * num.variance([2, 4, 4, 4, 5, 5, 7, 9]); // 4
372
+ *
373
+ * @param {Iterable<number>} values
374
+ * @param {VarianceOptions} [options]
375
+ * @returns {number}
376
+ */
377
+ function variance(values, options = {}) {
378
+ const list = [...values];
379
+ const divisor = options.sample ? list.length - 1 : list.length;
380
+ if (divisor <= 0) return 0;
381
+ const mean = sum(list) / list.length;
382
+ return list.reduce((acc, n) => acc + (n - mean) ** 2, 0) / divisor;
383
+ }
384
+
385
+ /**
386
+ * The standard deviation.
387
+ *
388
+ * @example
389
+ * num.stdDev([2, 4, 4, 4, 5, 5, 7, 9]); // 2
390
+ *
391
+ * @param {Iterable<number>} values
392
+ * @param {VarianceOptions} [options]
393
+ * @returns {number}
394
+ */
395
+ function stdDev(values, options = {}) {
396
+ return Math.sqrt(variance(values, options));
397
+ }
398
+
399
+ /**
400
+ * The value under which `p` percent of the values fall, interpolated like
401
+ * Excel's `PERCENTILE.INC`.
402
+ *
403
+ * @example
404
+ * num.percentile([1, 2, 3, 4, 5], 90); // 4.6
405
+ * num.percentile(responseTimes, 95); // p95 latency
406
+ *
407
+ * @param {Iterable<number>} values
408
+ * @param {number} p From 0 to 100.
409
+ * @returns {number} `NaN` for an empty list.
410
+ */
411
+ function percentile(values, p) {
412
+ const sorted = [...values].sort((a, b) => a - b);
413
+ if (!sorted.length) return NaN;
414
+ const rank = (clamp(p, 0, 100) / 100) * (sorted.length - 1);
415
+ const lower = Math.floor(rank);
416
+ const upper = Math.ceil(rank);
417
+ return round(sorted[lower] + (sorted[upper] - sorted[lower]) * (rank - lower), 12);
418
+ }
419
+
420
+ /**
421
+ * The smallest number. Unlike `Math.min(...array)`, huge arrays are fine.
422
+ *
423
+ * @param {Iterable<number>} values
424
+ * @returns {number | undefined} `undefined` for an empty list.
425
+ */
426
+ function min(values) {
427
+ let best;
428
+ for (const n of values) if (best === undefined || n < best) best = n;
429
+ return best;
430
+ }
431
+
432
+ /**
433
+ * The largest number. Unlike `Math.max(...array)`, huge arrays are fine.
434
+ *
435
+ * @param {Iterable<number>} values
436
+ * @returns {number | undefined} `undefined` for an empty list.
437
+ */
438
+ function max(values) {
439
+ let best;
440
+ for (const n of values) if (best === undefined || n > best) best = n;
441
+ return best;
442
+ }
443
+
444
+ /**
445
+ * What percentage `value` is of `total`. Returns `0` when `total` is `0`.
446
+ *
447
+ * @example
448
+ * num.percent(25, 200); // 12.5
449
+ * num.percent(1, 3, 1); // 33.3
450
+ *
451
+ * @param {number} value
452
+ * @param {number} total
453
+ * @param {number} [decimals=2]
454
+ * @returns {number}
455
+ */
456
+ function percent(value, total, decimals = 2) {
457
+ if (!total) return 0;
458
+ return round((value / total) * 100, decimals);
459
+ }
460
+
461
+ /**
462
+ * Greatest common divisor.
463
+ *
464
+ * @example
465
+ * num.gcd(12, 18, 27); // 3
466
+ *
467
+ * @param {...number} values
468
+ * @returns {number}
469
+ */
470
+ function gcd(...values) {
471
+ return values.reduce((a, b) => {
472
+ let x = Math.abs(a);
473
+ let y = Math.abs(b);
474
+ while (y) [x, y] = [y, x % y];
475
+ return x;
476
+ }, 0);
477
+ }
478
+
479
+ /**
480
+ * Least common multiple.
481
+ *
482
+ * @example
483
+ * num.lcm(2, 3, 4); // 12
484
+ *
485
+ * @param {...number} values
486
+ * @returns {number}
487
+ */
488
+ function lcm(...values) {
489
+ if (!values.length) return 0;
490
+ return values.reduce((a, b) => (a === 0 || b === 0 ? 0 : Math.abs(a * b) / gcd(a, b)));
491
+ }
492
+
493
+ /**
494
+ * Is it a prime number?
495
+ *
496
+ * @example
497
+ * num.isPrime(7); // true
498
+ * num.isPrime(1); // false
499
+ *
500
+ * @param {number} value
501
+ * @returns {boolean}
502
+ */
503
+ function isPrime(value) {
504
+ if (!Number.isInteger(value) || value < 2) return false;
505
+ if (value < 4) return true;
506
+ if (value % 2 === 0 || value % 3 === 0) return false;
507
+ for (let i = 5; i * i <= value; i += 6) {
508
+ if (value % i === 0 || value % (i + 2) === 0) return false;
509
+ }
510
+ return true;
511
+ }
512
+
513
+ /**
514
+ * `n!`. Pass `true` to get an exact bigint, since numbers lose precision
515
+ * past 18!.
516
+ *
517
+ * @example
518
+ * num.factorial(5); // 120
519
+ * num.factorial(25, true); // 15511210043330985984000000n
520
+ *
521
+ * @param {number} n
522
+ * @param {boolean} [asBigInt=false]
523
+ * @returns {number | bigint}
524
+ * @throws {RangeError} If `n` is negative or not an integer.
525
+ */
526
+ function factorial(n, asBigInt = false) {
527
+ if (!Number.isInteger(n) || n < 0) throw new RangeError("factorial() expects a non-negative integer");
528
+ if (asBigInt) {
529
+ let result = 1n;
530
+ for (let i = 2n; i <= BigInt(n); i++) result *= i;
531
+ return result;
532
+ }
533
+ let result = 1;
534
+ for (let i = 2; i <= n; i++) result *= i;
535
+ return result;
536
+ }
537
+
538
+ /**
539
+ * Is it even?
540
+ *
541
+ * @param {number} value
542
+ * @returns {boolean}
543
+ */
544
+ function isEven(value) {
545
+ return value % 2 === 0;
546
+ }
547
+
548
+ /**
549
+ * Is it odd? Negative numbers work too.
550
+ *
551
+ * @param {number} value
552
+ * @returns {boolean}
553
+ */
554
+ function isOdd(value) {
555
+ return Math.abs(value % 2) === 1;
556
+ }
557
+
558
+ /**
559
+ * Numbers from `start` up to `end` (excluded). With one argument, counts
560
+ * from 0. Goes down when `end` is smaller.
561
+ *
562
+ * @example
563
+ * num.range(4); // [0, 1, 2, 3]
564
+ * num.range(0, 10, 2); // [0, 2, 4, 6, 8]
565
+ * num.range(5, 0); // [5, 4, 3, 2, 1]
566
+ *
567
+ * @param {number} start
568
+ * @param {number} [end]
569
+ * @param {number} [step] Defaults to `1`, or `-1` when counting down.
570
+ * @returns {number[]}
571
+ */
572
+ function range(start, end, step) {
573
+ let from = start;
574
+ let to = end;
575
+ if (to === undefined) {
576
+ to = from;
577
+ from = 0;
578
+ }
579
+ const inc = step ?? (to < from ? -1 : 1);
580
+ if (inc === 0 || (inc > 0 && from > to) || (inc < 0 && from < to)) return [];
581
+ const decimals = Math.max((String(inc).split(".")[1] ?? "").length, (String(from).split(".")[1] ?? "").length);
582
+ const out = [];
583
+ const count = Math.ceil((to - from) / inc);
584
+ for (let i = 0; i < count; i++) out.push(decimals ? round(from + i * inc, decimals) : from + i * inc);
585
+ return out;
586
+ }
587
+
588
+ /**
589
+ * Reads a number from anything, ignoring trailing text like units.
590
+ *
591
+ * @example
592
+ * num.parse("42px"); // 42
593
+ * num.parse(" -3.5 kg"); // -3.5
594
+ * num.parse("nope", 0); // 0
595
+ *
596
+ * @param {unknown} value
597
+ * @param {number} [fallback=NaN] Returned when there's no number to read.
598
+ * @returns {number}
599
+ */
600
+ function parse(value, fallback = NaN) {
601
+ if (typeof value === "number") return Number.isNaN(value) ? fallback : value;
602
+ if (typeof value === "bigint") return Number(value);
603
+ const match = /[-+]?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/i.exec(String(value));
604
+ const parsed = match ? Number(match[0]) : NaN;
605
+ return Number.isNaN(parsed) ? fallback : parsed;
606
+ }
607
+
608
+ /**
609
+ * Turns a size like `"10 MB"` into bytes. The opposite of `formatBytes()`.
610
+ *
611
+ * @example
612
+ * num.parseBytes("1.5 KB"); // 1536
613
+ * num.parseBytes("2 GiB"); // 2147483648
614
+ * num.parseBytes("1 KB", { binary: false }); // 1000
615
+ *
616
+ * @param {string | number} value A size like `"512kb"` or `"1.5G"`, or a number of bytes.
617
+ * @param {ParseBytesOptions} [options]
618
+ * @returns {number} Bytes, or `NaN` if it can't be read.
619
+ */
620
+ function parseBytes(value, options = {}) {
621
+ if (typeof value === "number") return value;
622
+ const match = /^\s*([-+]?(?:\d+\.?\d*|\.\d+))\s*([kmgtpezy]?)(i?)(b?)(?:ytes?)?\s*$/i.exec(String(value));
623
+ if (!match) return NaN;
624
+ const [, amount, prefix, iec] = match;
625
+ const base = iec || options.binary !== false ? 1024 : 1000;
626
+ const power = prefix ? "kmgtpezy".indexOf(prefix.toLowerCase()) + 1 : 0;
627
+ return Math.floor(Number(amount) * base ** power);
628
+ }
629
+
630
+ /**
631
+ * Formats a byte count as a readable size.
632
+ *
633
+ * @example
634
+ * num.formatBytes(1536); // "1.5 KB"
635
+ * num.formatBytes(1000, { binary: false }); // "1 KB"
636
+ * num.formatBytes(1536, { locale: "fr", units: ["o", "Ko", "Mo", "Go"] }); // "1,5 Ko"
637
+ *
638
+ * @param {number} bytes
639
+ * @param {FormatBytesOptions} [options]
640
+ * @returns {string}
641
+ */
642
+ function formatBytes(bytes, options = {}) {
643
+ const units = options.units ?? (options.iec ? _IEC_UNITS : _SIZE_UNITS);
644
+ if (!Number.isFinite(bytes) || bytes === 0) return `0 ${units[0]}`;
645
+ const decimals = options.decimals ?? 1;
646
+ const base = options.binary === false ? 1000 : 1024;
647
+ const sign = bytes < 0 ? "-" : "";
648
+ const abs = Math.abs(bytes);
649
+ const i = Math.min(Math.floor(Math.log(abs) / Math.log(base)), units.length - 1);
650
+ const amount = round(abs / base ** i, decimals);
651
+ const locale = options.locale ?? (_config.locale === "en" ? undefined : _config.locale);
652
+ const text = locale ? new Intl.NumberFormat(locale, { maximumFractionDigits: decimals }).format(amount) : String(amount);
653
+ return `${sign}${text} ${units[i]}`;
654
+ }
655
+
656
+ /**
657
+ * Formats a number for display, with `Intl.NumberFormat` doing the work:
658
+ * separators, decimals, compact notation, percentages, units.
659
+ *
660
+ * @example
661
+ * num.format(1234567.891); // "1,234,567.891"
662
+ * num.format(1234567.891, { locale: "fr" }); // "1 234 567,891"
663
+ * num.format(0.256, { style: "percent" }); // "26%"
664
+ * num.format(1500, { compact: true }); // "1.5K"
665
+ * num.format(3.5, { unit: "kilometer" }); // "3.5 km"
666
+ *
667
+ * @param {number | bigint} value
668
+ * @param {FormatNumberOptions} [options]
669
+ * @returns {string}
670
+ */
671
+ function format(value, options = {}) {
672
+ /** @type {Intl.NumberFormatOptions} */
673
+ const intl = {};
674
+ if (options.decimals !== undefined) {
675
+ intl.minimumFractionDigits = options.decimals;
676
+ intl.maximumFractionDigits = options.decimals;
677
+ }
678
+ if (options.minDecimals !== undefined) intl.minimumFractionDigits = options.minDecimals;
679
+ if (options.maxDecimals !== undefined) intl.maximumFractionDigits = options.maxDecimals;
680
+ if (options.compact) {
681
+ intl.notation = "compact";
682
+ intl.compactDisplay = "short";
683
+ }
684
+ if (options.style === "percent") intl.style = "percent";
685
+ if (options.unit) {
686
+ intl.style = "unit";
687
+ intl.unit = options.unit;
688
+ intl.unitDisplay = options.unitDisplay ?? "short";
689
+ }
690
+ if (options.signDisplay) intl.signDisplay = options.signDisplay;
691
+ if (options.grouping === false) intl.useGrouping = false;
692
+ return new Intl.NumberFormat(options.locale ?? _config.locale, intl).format(value);
693
+ }
694
+
695
+ /**
696
+ * Formats an amount of money.
697
+ *
698
+ * @example
699
+ * num.currency(1234.5, "USD"); // "$1,234.50"
700
+ * num.currency(1234.5, "EUR", { locale: "fr" }); // "1 234,50 €"
701
+ * num.currency(-5, "USD", { accounting: true }); // "($5.00)"
702
+ *
703
+ * @param {number | bigint} value
704
+ * @param {string} code A currency code like `"EUR"`, `"USD"` or `"JPY"`.
705
+ * @param {CurrencyOptions} [options]
706
+ * @returns {string}
707
+ */
708
+ function currency(value, code, options = {}) {
709
+ /** @type {Intl.NumberFormatOptions} */
710
+ const intl = { style: "currency", currency: code, currencyDisplay: options.display ?? "symbol" };
711
+ if (options.decimals !== undefined) {
712
+ intl.minimumFractionDigits = options.decimals;
713
+ intl.maximumFractionDigits = options.decimals;
714
+ }
715
+ if (options.accounting) intl.currencySign = "accounting";
716
+ return new Intl.NumberFormat(options.locale ?? _config.locale, intl).format(value);
717
+ }
718
+
719
+ /**
720
+ * Shortens big numbers with `K`, `M`, `B` or `T`. For other languages, use
721
+ * `format(value, { compact: true, locale })`.
722
+ *
723
+ * @example
724
+ * num.abbreviate(1500); // "1.5K"
725
+ * num.abbreviate(2_400_000); // "2.4M"
726
+ *
727
+ * @param {number} value
728
+ * @param {number} [decimals=1]
729
+ * @returns {string}
730
+ */
731
+ function abbreviate(value, decimals = 1) {
732
+ const sign = value < 0 ? "-" : "";
733
+ const abs = Math.abs(value);
734
+ for (const [limit, suffix] of /** @type {Array<[number, string]>} */ ([[1e12, "T"], [1e9, "B"], [1e6, "M"], [1e3, "K"]])) {
735
+ if (abs >= limit) return `${sign}${round(abs / limit, decimals)}${suffix}`;
736
+ }
737
+ return `${sign}${round(abs, decimals)}`;
738
+ }
739
+
740
+ /**
741
+ * Adds thousands separators, leaving decimals alone.
742
+ *
743
+ * @example
744
+ * num.thousands(1234567.89, " "); // "1 234 567.89"
745
+ *
746
+ * @param {number | string} value
747
+ * @param {string} [separator=","]
748
+ * @returns {string}
749
+ */
750
+ function thousands(value, separator = ",") {
751
+ const [int, dec] = String(value).split(".");
752
+ const grouped = int.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
753
+ return dec === undefined ? grouped : `${grouped}.${dec}`;
754
+ }
755
+
756
+ /**
757
+ * Adds the ordinal suffix: 1st, 2nd, 3rd... in several languages.
758
+ *
759
+ * @example
760
+ * num.ordinal(22); // "22nd"
761
+ * num.ordinal(1, { locale: "fr" }); // "1er"
762
+ * num.ordinal(3, { locale: "de" }); // "3."
763
+ *
764
+ * @param {number} value
765
+ * @param {OrdinalOptions} [options]
766
+ * @returns {string}
767
+ */
768
+ function ordinal(value, options = {}) {
769
+ const locale = options.locale ?? _config.locale;
770
+ const language = locale.split(/[-_]/)[0].toLowerCase();
771
+ const suffixes = options.suffixes ?? _ORDINAL_SUFFIXES[language] ?? _ORDINAL_SUFFIXES.en;
772
+ let category;
773
+ try {
774
+ category = new Intl.PluralRules(locale, { type: "ordinal" }).select(value);
775
+ } catch {
776
+ category = new Intl.PluralRules("en", { type: "ordinal" }).select(value);
777
+ }
778
+ return `${value}${suffixes[category] ?? suffixes.other ?? ""}`;
779
+ }
780
+
781
+ /**
782
+ * Sets the default language of `format`, `currency`, `percent`,
783
+ * `abbreviate`, `ordinal` and `formatBytes`.
784
+ *
785
+ * @example
786
+ * num.setLocale("fr");
787
+ * num.currency(1234.5, "EUR"); // "1 234,50 €"
788
+ *
789
+ * @param {string} locale Like `"fr"`, `"en-GB"` or `"pt-BR"`.
790
+ * @returns {typeof import("./Num")} The module, so calls chain.
791
+ */
792
+ function setLocale(locale) {
793
+ _config.locale = locale;
794
+ return module.exports;
795
+ }
796
+
797
+ /**
798
+ * The current default language.
799
+ *
800
+ * @example
801
+ * num.getLocale(); // "en"
802
+ *
803
+ * @returns {string}
804
+ */
805
+ function getLocale() {
806
+ return _config.locale;
807
+ }
808
+
809
+ module.exports = {
810
+ setLocale,
811
+ getLocale,
812
+ clamp,
813
+ inRange,
814
+ wrap,
815
+ round,
816
+ floor,
817
+ ceil,
818
+ snap,
819
+ approxEqual,
820
+ lerp,
821
+ mapRange,
822
+ random,
823
+ randomInt,
824
+ sum,
825
+ average,
826
+ median,
827
+ mode,
828
+ variance,
829
+ stdDev,
830
+ percentile,
831
+ min,
832
+ max,
833
+ percent,
834
+ gcd,
835
+ lcm,
836
+ isPrime,
837
+ factorial,
838
+ isEven,
839
+ isOdd,
840
+ range,
841
+ parse,
842
+ parseBytes,
843
+ formatBytes,
844
+ format,
845
+ currency,
846
+ abbreviate,
847
+ thousands,
848
+ ordinal,
849
+ };