@ix-xs/node-comfort 2.0.0 → 2.1.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 +150 -0
  2. package/README.md +32 -30
  3. package/index.js +3 -3
  4. package/package.json +8 -7
  5. package/src/Arr.js +222 -58
  6. package/src/Async.js +322 -90
  7. package/src/Cache.js +148 -44
  8. package/src/Checker.js +257 -26
  9. package/src/Cli.js +404 -145
  10. package/src/Color.js +224 -83
  11. package/src/Crypto.js +481 -106
  12. package/src/Emitter.js +104 -37
  13. package/src/Env.js +176 -42
  14. package/src/FS.js +203 -60
  15. package/src/Func.js +226 -76
  16. package/src/Http.js +311 -149
  17. package/src/Id.js +281 -96
  18. package/src/Logger.js +302 -78
  19. package/src/Num.js +182 -60
  20. package/src/Obj.js +340 -125
  21. package/src/SQLite.js +426 -105
  22. package/src/Schema.js +602 -87
  23. package/src/Str.js +316 -86
  24. package/src/Sys.js +197 -54
  25. package/src/Time.js +572 -317
  26. package/src/Utils.js +62 -41
  27. package/src/errors.js +54 -11
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +59 -17
  30. package/types/src/Arr.d.ts +78 -0
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +52 -1
  33. package/types/src/Async.d.ts.map +1 -1
  34. package/types/src/Cache.d.ts +33 -7
  35. package/types/src/Cache.d.ts.map +1 -1
  36. package/types/src/Checker.d.ts +155 -2
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +97 -15
  39. package/types/src/Cli.d.ts.map +1 -1
  40. package/types/src/Color.d.ts +7 -1
  41. package/types/src/Color.d.ts.map +1 -1
  42. package/types/src/Crypto.d.ts +99 -13
  43. package/types/src/Crypto.d.ts.map +1 -1
  44. package/types/src/Emitter.d.ts +36 -17
  45. package/types/src/Emitter.d.ts.map +1 -1
  46. package/types/src/Env.d.ts +31 -1
  47. package/types/src/Env.d.ts.map +1 -1
  48. package/types/src/FS.d.ts +36 -5
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +147 -19
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +113 -43
  53. package/types/src/Http.d.ts.map +1 -1
  54. package/types/src/Id.d.ts +60 -8
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +53 -2
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +48 -7
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +67 -4
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +123 -14
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +256 -7
  65. package/types/src/Schema.d.ts.map +1 -1
  66. package/types/src/Str.d.ts +49 -1
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +51 -6
  69. package/types/src/Sys.d.ts.map +1 -1
  70. package/types/src/Time.d.ts +130 -34
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +15 -11
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +27 -3
  75. package/types/src/errors.d.ts.map +1 -1
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
package/src/Num.js CHANGED
@@ -13,6 +13,8 @@
13
13
  * num.percentile(latencies, 95);
14
14
  */
15
15
 
16
+ const { intl, preciseSum } = require("./internal/shared.js");
17
+
16
18
  /** The default language, changed with `setLocale()`. */
17
19
  const _config = { locale: "en" };
18
20
 
@@ -88,9 +90,32 @@ const _shift = (value, exp) => {
88
90
  */
89
91
  const _roundWith = (value, decimals, fn) => {
90
92
  if (!Number.isFinite(value)) return value;
93
+ if (decimals === 0) return fn(value);
94
+ if (decimals > 0 && Number.isInteger(value)) return value;
91
95
  return _shift(fn(_shift(value, decimals)), -decimals);
92
96
  };
93
97
 
98
+ /**
99
+ * How many decimals a number is written with, exponent notation included.
100
+ * @param {number} n
101
+ * @returns {number}
102
+ */
103
+ const _decimalsOf = (n) => {
104
+ if (Number.isInteger(n) || !Number.isFinite(n)) return 0;
105
+ const text = String(n);
106
+ const exponent = text.indexOf("e-");
107
+ if (exponent === -1) return text.length - text.indexOf(".") - 1;
108
+ const mantissa = text.slice(0, exponent);
109
+ return Number(text.slice(exponent + 2)) + (mantissa.includes(".") ? mantissa.length - mantissa.indexOf(".") - 1 : 0);
110
+ };
111
+
112
+ /** @param {number} n */
113
+ const _halfAwayFromZero = (n) => Math.sign(n) * Math.round(Math.abs(n)) || 0;
114
+
115
+ /** @type {Array<[number, string]>} */
116
+ const _ABBREVIATIONS = [[1e12, "T"], [1e9, "B"], [1e6, "M"], [1e3, "K"]];
117
+ const _ORDINAL = { type: "ordinal" };
118
+
94
119
  const _SIZE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
95
120
  const _IEC_UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
96
121
 
@@ -170,7 +195,7 @@ function wrap(value, min, max) {
170
195
  * @returns {number}
171
196
  */
172
197
  function round(value, decimals = 0) {
173
- return _roundWith(value, decimals, (n) => Math.sign(n) * Math.round(Math.abs(n)) || 0);
198
+ return _roundWith(value, decimals, _halfAwayFromZero);
174
199
  }
175
200
 
176
201
  /**
@@ -214,8 +239,7 @@ function ceil(value, decimals = 0) {
214
239
  */
215
240
  function snap(value, step) {
216
241
  if (!step) return value;
217
- const decimals = (String(step).split(".")[1] ?? "").length;
218
- return round(Math.round(value / step) * step, decimals);
242
+ return round(Math.round(value / step) * step, _decimalsOf(step));
219
243
  }
220
244
 
221
245
  /**
@@ -302,17 +326,31 @@ function randomInt(min, max) {
302
326
  }
303
327
 
304
328
  /**
305
- * Adds up numbers.
329
+ * Adds up numbers, without the drift of a naive loop.
306
330
  *
307
331
  * @example
308
- * num.sum([1, 2, 3]); // 6
332
+ * num.sum([1, 2, 3]); // 6
333
+ * num.sum([0.1, 0.2, 0.3]); // 0.6, where a plain loop gives 0.6000000000000001
309
334
  *
310
335
  * @param {Iterable<number>} values
311
336
  * @returns {number}
312
337
  */
313
338
  function sum(values) {
314
- let total = 0;
315
- for (const n of values) total += n;
339
+ return preciseSum(values);
340
+ }
341
+
342
+ /**
343
+ * Multiplies numbers. An empty list gives `1`.
344
+ *
345
+ * @example
346
+ * num.product([2, 3, 4]); // 24
347
+ *
348
+ * @param {Iterable<number>} values
349
+ * @returns {number}
350
+ */
351
+ function product(values) {
352
+ let total = 1;
353
+ for (const n of values) total *= n;
316
354
  return total;
317
355
  }
318
356
 
@@ -326,8 +364,8 @@ function sum(values) {
326
364
  * @returns {number}
327
365
  */
328
366
  function average(values) {
329
- const list = [...values];
330
- return list.length ? sum(list) / list.length : 0;
367
+ const list = Array.isArray(values) ? values : [...values];
368
+ return list.length ? preciseSum(list) / list.length : 0;
331
369
  }
332
370
 
333
371
  /**
@@ -359,9 +397,16 @@ function median(values) {
359
397
  function mode(values) {
360
398
  /** @type {Map<number, number>} */
361
399
  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);
400
+ let best = 0;
401
+ for (const n of values) {
402
+ const count = (counts.get(n) ?? 0) + 1;
403
+ counts.set(n, count);
404
+ if (count > best) best = count;
405
+ }
406
+ /** @type {number[]} */
407
+ const out = [];
408
+ for (const [n, count] of counts) if (count === best) out.push(n);
409
+ return out;
365
410
  }
366
411
 
367
412
  /**
@@ -375,11 +420,13 @@ function mode(values) {
375
420
  * @returns {number}
376
421
  */
377
422
  function variance(values, options = {}) {
378
- const list = [...values];
423
+ const list = Array.isArray(values) ? values : [...values];
379
424
  const divisor = options.sample ? list.length - 1 : list.length;
380
425
  if (divisor <= 0) return 0;
381
- const mean = sum(list) / list.length;
382
- return list.reduce((acc, n) => acc + (n - mean) ** 2, 0) / divisor;
426
+ const mean = preciseSum(list) / list.length;
427
+ let total = 0;
428
+ for (let i = 0; i < list.length; i++) total += (list[i] - mean) ** 2;
429
+ return total / divisor;
383
430
  }
384
431
 
385
432
  /**
@@ -419,25 +466,33 @@ function percentile(values, p) {
419
466
 
420
467
  /**
421
468
  * The smallest number. Unlike `Math.min(...array)`, huge arrays are fine.
469
+ * Like `Math.min`, a `NaN` in the list gives `NaN`.
422
470
  *
423
471
  * @param {Iterable<number>} values
424
472
  * @returns {number | undefined} `undefined` for an empty list.
425
473
  */
426
474
  function min(values) {
427
475
  let best;
428
- for (const n of values) if (best === undefined || n < best) best = n;
476
+ for (const n of values) {
477
+ if (n !== n) return NaN;
478
+ if (best === undefined || n < best) best = n;
479
+ }
429
480
  return best;
430
481
  }
431
482
 
432
483
  /**
433
484
  * The largest number. Unlike `Math.max(...array)`, huge arrays are fine.
485
+ * Like `Math.max`, a `NaN` in the list gives `NaN`.
434
486
  *
435
487
  * @param {Iterable<number>} values
436
488
  * @returns {number | undefined} `undefined` for an empty list.
437
489
  */
438
490
  function max(values) {
439
491
  let best;
440
- for (const n of values) if (best === undefined || n > best) best = n;
492
+ for (const n of values) {
493
+ if (n !== n) return NaN;
494
+ if (best === undefined || n > best) best = n;
495
+ }
441
496
  return best;
442
497
  }
443
498
 
@@ -458,6 +513,25 @@ function percent(value, total, decimals = 2) {
458
513
  return round((value / total) * 100, decimals);
459
514
  }
460
515
 
516
+ /**
517
+ * How much a value changed, in percent. Going from `0` to anything else is
518
+ * an infinite change.
519
+ *
520
+ * @example
521
+ * num.percentChange(80, 100); // 25
522
+ * num.percentChange(100, 80); // -20
523
+ *
524
+ * @param {number} from
525
+ * @param {number} to
526
+ * @param {number} [decimals=2]
527
+ * @returns {number}
528
+ */
529
+ function percentChange(from, to, decimals = 2) {
530
+ if (from === to) return 0;
531
+ if (!from) return to > 0 ? Infinity : -Infinity;
532
+ return round(((to - from) / Math.abs(from)) * 100, decimals);
533
+ }
534
+
461
535
  /**
462
536
  * Greatest common divisor.
463
537
  *
@@ -468,12 +542,18 @@ function percent(value, total, decimals = 2) {
468
542
  * @returns {number}
469
543
  */
470
544
  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);
545
+ let result = 0;
546
+ for (const value of values) {
547
+ let x = Math.abs(result);
548
+ let y = Math.abs(value);
549
+ while (y) {
550
+ const rest = x % y;
551
+ x = y;
552
+ y = rest;
553
+ }
554
+ result = x;
555
+ }
556
+ return result;
477
557
  }
478
558
 
479
559
  /**
@@ -487,7 +567,8 @@ function gcd(...values) {
487
567
  */
488
568
  function lcm(...values) {
489
569
  if (!values.length) return 0;
490
- return values.reduce((a, b) => (a === 0 || b === 0 ? 0 : Math.abs(a * b) / gcd(a, b)));
570
+ // Dividing first keeps the intermediate value small.
571
+ return values.reduce((a, b) => (a === 0 || b === 0 ? 0 : Math.abs((a / gcd(a, b)) * b)));
491
572
  }
492
573
 
493
574
  /**
@@ -578,10 +659,13 @@ function range(start, end, step) {
578
659
  }
579
660
  const inc = step ?? (to < from ? -1 : 1);
580
661
  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);
662
+ const decimals = Math.max(_decimalsOf(inc), _decimalsOf(from));
663
+ // Rounding the ratio keeps 1.1 / 0.1 = 11.000000000000002 from adding an item.
664
+ const count = Math.ceil(round((to - from) / inc, 9));
665
+ if (!Number.isFinite(count) || count > 0xffffffff) throw new RangeError("range() would produce too many numbers");
666
+ /** @type {number[]} */
667
+ const out = new Array(count);
668
+ for (let i = 0; i < count; i++) out[i] = decimals ? round(from + i * inc, decimals) : from + i * inc;
585
669
  return out;
586
670
  }
587
671
 
@@ -644,13 +728,22 @@ function formatBytes(bytes, options = {}) {
644
728
  if (!Number.isFinite(bytes) || bytes === 0) return `0 ${units[0]}`;
645
729
  const decimals = options.decimals ?? 1;
646
730
  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);
731
+ const last = units.length - 1;
732
+ let amount = Math.abs(bytes);
733
+ let unit = 0;
734
+ while (amount >= base && unit < last) {
735
+ amount /= base;
736
+ unit++;
737
+ }
738
+ amount = round(amount, decimals);
739
+ // Rounding can reach the next unit: 1023.99 KB reads better as 1 MB.
740
+ if (amount >= base && unit < last) {
741
+ amount = round(amount / base, decimals);
742
+ unit++;
743
+ }
651
744
  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]}`;
745
+ const text = locale ? intl.number(locale, { maximumFractionDigits: decimals }).format(amount) : String(amount);
746
+ return `${bytes < 0 ? "-" : ""}${text} ${units[unit]}`;
654
747
  }
655
748
 
656
749
  /**
@@ -670,26 +763,26 @@ function formatBytes(bytes, options = {}) {
670
763
  */
671
764
  function format(value, options = {}) {
672
765
  /** @type {Intl.NumberFormatOptions} */
673
- const intl = {};
766
+ const format = {};
674
767
  if (options.decimals !== undefined) {
675
- intl.minimumFractionDigits = options.decimals;
676
- intl.maximumFractionDigits = options.decimals;
768
+ format.minimumFractionDigits = options.decimals;
769
+ format.maximumFractionDigits = options.decimals;
677
770
  }
678
- if (options.minDecimals !== undefined) intl.minimumFractionDigits = options.minDecimals;
679
- if (options.maxDecimals !== undefined) intl.maximumFractionDigits = options.maxDecimals;
771
+ if (options.minDecimals !== undefined) format.minimumFractionDigits = options.minDecimals;
772
+ if (options.maxDecimals !== undefined) format.maximumFractionDigits = options.maxDecimals;
680
773
  if (options.compact) {
681
- intl.notation = "compact";
682
- intl.compactDisplay = "short";
774
+ format.notation = "compact";
775
+ format.compactDisplay = "short";
683
776
  }
684
- if (options.style === "percent") intl.style = "percent";
777
+ if (options.style === "percent") format.style = "percent";
685
778
  if (options.unit) {
686
- intl.style = "unit";
687
- intl.unit = options.unit;
688
- intl.unitDisplay = options.unitDisplay ?? "short";
779
+ format.style = "unit";
780
+ format.unit = options.unit;
781
+ format.unitDisplay = options.unitDisplay ?? "short";
689
782
  }
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);
783
+ if (options.signDisplay) format.signDisplay = options.signDisplay;
784
+ if (options.grouping === false) format.useGrouping = false;
785
+ return intl.number(options.locale ?? _config.locale, format).format(value);
693
786
  }
694
787
 
695
788
  /**
@@ -707,13 +800,13 @@ function format(value, options = {}) {
707
800
  */
708
801
  function currency(value, code, options = {}) {
709
802
  /** @type {Intl.NumberFormatOptions} */
710
- const intl = { style: "currency", currency: code, currencyDisplay: options.display ?? "symbol" };
803
+ const format = { style: "currency", currency: code, currencyDisplay: options.display ?? "symbol" };
711
804
  if (options.decimals !== undefined) {
712
- intl.minimumFractionDigits = options.decimals;
713
- intl.maximumFractionDigits = options.decimals;
805
+ format.minimumFractionDigits = options.decimals;
806
+ format.maximumFractionDigits = options.decimals;
714
807
  }
715
- if (options.accounting) intl.currencySign = "accounting";
716
- return new Intl.NumberFormat(options.locale ?? _config.locale, intl).format(value);
808
+ if (options.accounting) format.currencySign = "accounting";
809
+ return intl.number(options.locale ?? _config.locale, format).format(value);
717
810
  }
718
811
 
719
812
  /**
@@ -729,12 +822,19 @@ function currency(value, code, options = {}) {
729
822
  * @returns {string}
730
823
  */
731
824
  function abbreviate(value, decimals = 1) {
825
+ if (!Number.isFinite(value)) return String(value);
732
826
  const sign = value < 0 ? "-" : "";
733
827
  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}`;
828
+ for (let i = 0; i < _ABBREVIATIONS.length; i++) {
829
+ const [limit, suffix] = _ABBREVIATIONS[i];
830
+ if (abs < limit) continue;
831
+ const rounded = round(abs / limit, decimals);
832
+ // Rounding can reach the next suffix: 999,950 is 1M, not 1000K.
833
+ if (rounded >= 1000 && i > 0) return `${sign}${round(abs / _ABBREVIATIONS[i - 1][0], decimals)}${_ABBREVIATIONS[i - 1][1]}`;
834
+ return `${sign}${rounded}${suffix}`;
736
835
  }
737
- return `${sign}${round(abs, decimals)}`;
836
+ const rounded = round(abs, decimals);
837
+ return rounded >= 1000 ? `${sign}${round(abs / 1000, decimals)}K` : `${sign}${rounded}`;
738
838
  }
739
839
 
740
840
  /**
@@ -753,6 +853,25 @@ function thousands(value, separator = ",") {
753
853
  return dec === undefined ? grouped : `${grouped}.${dec}`;
754
854
  }
755
855
 
856
+ /**
857
+ * Pads the integer part with zeros (or another character).
858
+ *
859
+ * @example
860
+ * num.pad(7, 3); // "007"
861
+ * num.pad(-7, 3); // "-007"
862
+ * num.pad(3.5, 2); // "03.5"
863
+ *
864
+ * @param {number} value
865
+ * @param {number} length Digits wanted before the decimal point.
866
+ * @param {string} [char="0"]
867
+ * @returns {string}
868
+ */
869
+ function pad(value, length, char = "0") {
870
+ const [int, dec] = String(Math.abs(value)).split(".");
871
+ const sign = value < 0 || Object.is(value, -0) ? "-" : "";
872
+ return `${sign}${int.padStart(length, char)}${dec === undefined ? "" : `.${dec}`}`;
873
+ }
874
+
756
875
  /**
757
876
  * Adds the ordinal suffix: 1st, 2nd, 3rd... in several languages.
758
877
  *
@@ -771,16 +890,16 @@ function ordinal(value, options = {}) {
771
890
  const suffixes = options.suffixes ?? _ORDINAL_SUFFIXES[language] ?? _ORDINAL_SUFFIXES.en;
772
891
  let category;
773
892
  try {
774
- category = new Intl.PluralRules(locale, { type: "ordinal" }).select(value);
893
+ category = intl.plural(locale, _ORDINAL).select(value);
775
894
  } catch {
776
- category = new Intl.PluralRules("en", { type: "ordinal" }).select(value);
895
+ category = intl.plural("en", _ORDINAL).select(value);
777
896
  }
778
897
  return `${value}${suffixes[category] ?? suffixes.other ?? ""}`;
779
898
  }
780
899
 
781
900
  /**
782
- * Sets the default language of `format`, `currency`, `percent`,
783
- * `abbreviate`, `ordinal` and `formatBytes`.
901
+ * Sets the default language of `format`, `currency`, `ordinal` and
902
+ * `formatBytes`.
784
903
  *
785
904
  * @example
786
905
  * num.setLocale("fr");
@@ -822,6 +941,7 @@ module.exports = {
822
941
  random,
823
942
  randomInt,
824
943
  sum,
944
+ product,
825
945
  average,
826
946
  median,
827
947
  mode,
@@ -831,6 +951,7 @@ module.exports = {
831
951
  min,
832
952
  max,
833
953
  percent,
954
+ percentChange,
834
955
  gcd,
835
956
  lcm,
836
957
  isPrime,
@@ -845,5 +966,6 @@ module.exports = {
845
966
  currency,
846
967
  abbreviate,
847
968
  thousands,
969
+ pad,
848
970
  ordinal,
849
971
  };