@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/Arr.js CHANGED
@@ -48,6 +48,16 @@
48
48
  * @property {boolean} hasNext
49
49
  */
50
50
 
51
+ /**
52
+ * Options for `toSentence()`.
53
+ * @typedef {object} ToSentenceOptions
54
+ * @property {string} [locale] Language of the joining word. Defaults to `"en"`.
55
+ * @property {"conjunction" | "disjunction" | "unit"} [type] `"conjunction"` joins with "and", `"disjunction"` with "or", `"unit"` with commas only. Defaults to `"conjunction"`.
56
+ * @property {"long" | "short" | "narrow"} [style] Defaults to `"long"`.
57
+ */
58
+
59
+ const { intl, preciseSum } = require("./internal/shared.js");
60
+
51
61
  /**
52
62
  * @param {unknown} iteratee
53
63
  * @returns {(item: any, index: number) => any}
@@ -245,12 +255,16 @@ function unique(array, iteratee) {
245
255
  */
246
256
  function groupBy(array, iteratee) {
247
257
  const fn = _fn(iteratee);
248
- /** @type {Record<PropertyKey, T[]>} */
249
- const out = {};
250
- array.forEach((item, i) => {
251
- (out[_key(fn(item, i))] ??= []).push(item);
252
- });
253
- return out;
258
+ // A Map, so keys like "constructor" or "__proto__" are plain data.
259
+ /** @type {Map<PropertyKey, T[]>} */
260
+ const groups = new Map();
261
+ for (let i = 0; i < array.length; i++) {
262
+ const key = _key(fn(array[i], i));
263
+ const group = groups.get(key);
264
+ if (group) group.push(array[i]);
265
+ else groups.set(key, [array[i]]);
266
+ }
267
+ return Object.fromEntries(groups);
254
268
  }
255
269
 
256
270
  /**
@@ -267,12 +281,10 @@ function groupBy(array, iteratee) {
267
281
  */
268
282
  function keyBy(array, iteratee) {
269
283
  const fn = _fn(iteratee);
270
- /** @type {Record<PropertyKey, T>} */
271
- const out = {};
272
- array.forEach((item, i) => {
273
- out[_key(fn(item, i))] = item;
274
- });
275
- return out;
284
+ /** @type {Map<PropertyKey, T>} */
285
+ const out = new Map();
286
+ for (let i = 0; i < array.length; i++) out.set(_key(fn(array[i], i)), array[i]);
287
+ return Object.fromEntries(out);
276
288
  }
277
289
 
278
290
  /**
@@ -311,13 +323,13 @@ function toMap(array, key, value) {
311
323
  */
312
324
  function countBy(array, iteratee) {
313
325
  const fn = _fn(iteratee);
314
- /** @type {Record<PropertyKey, number>} */
315
- const out = {};
316
- array.forEach((item, i) => {
317
- const key = _key(fn(item, i));
318
- out[key] = (out[key] ?? 0) + 1;
319
- });
320
- return out;
326
+ /** @type {Map<PropertyKey, number>} */
327
+ const out = new Map();
328
+ for (let i = 0; i < array.length; i++) {
329
+ const key = _key(fn(array[i], i));
330
+ out.set(key, (out.get(key) ?? 0) + 1);
331
+ }
332
+ return Object.fromEntries(out);
321
333
  }
322
334
 
323
335
  /**
@@ -394,34 +406,46 @@ function pluck(array, key) {
394
406
  function sortBy(array, keys, order = "asc") {
395
407
  const options = typeof order === "string" ? { order } : order;
396
408
  const defaultDir = options.order === "desc" ? -1 : 1;
397
- const collator = options.natural ? new Intl.Collator(options.locale, { numeric: true, sensitivity: "base" }) : null;
409
+ const collator = options.natural ? intl.collator(options.locale, { numeric: true, sensitivity: "base" }) : null;
398
410
  const isTuple = (/** @type {unknown} */ k) => Array.isArray(k) && k.length === 2 && (k[1] === "asc" || k[1] === "desc");
399
411
  const list = Array.isArray(keys) && !isTuple(keys) ? keys : [keys];
400
412
  const comparators = list.map((k) => {
401
413
  const [iteratee, dir] = isTuple(k) ? /** @type {[unknown, string]} */ (k) : [k, undefined];
402
414
  return { fn: _fn(iteratee), dir: dir === undefined ? defaultDir : dir === "desc" ? -1 : 1 };
403
415
  });
404
- return array
405
- .map((item, index) => ({ item, index }))
406
- .sort((a, b) => {
407
- for (const { fn, dir } of comparators) {
408
- const va = fn(a.item, a.index);
409
- const vb = fn(b.item, b.index);
410
- const aNil = va === null || va === undefined;
411
- const bNil = vb === null || vb === undefined;
412
- if (aNil || bNil) {
413
- if (aNil && bNil) continue;
414
- return aNil ? 1 : -1;
415
- }
416
- let result = 0;
417
- if (collator && typeof va === "string" && typeof vb === "string") result = collator.compare(va, vb);
418
- else if (va < vb) result = -1;
419
- else if (va > vb) result = 1;
420
- if (result) return result * dir;
416
+ const size = array.length;
417
+ const width = comparators.length;
418
+ // Each sort value is computed once, not on every comparison.
419
+ /** @type {any[]} */
420
+ const values = new Array(size * width);
421
+ /** @type {number[]} */
422
+ const indexes = new Array(size);
423
+ for (let i = 0; i < size; i++) {
424
+ indexes[i] = i;
425
+ for (let k = 0; k < width; k++) values[i * width + k] = comparators[k].fn(array[i], i);
426
+ }
427
+ indexes.sort((a, b) => {
428
+ for (let k = 0; k < width; k++) {
429
+ const va = values[a * width + k];
430
+ const vb = values[b * width + k];
431
+ const aNil = va === null || va === undefined;
432
+ const bNil = vb === null || vb === undefined;
433
+ if (aNil || bNil) {
434
+ if (aNil && bNil) continue;
435
+ return aNil ? 1 : -1;
421
436
  }
422
- return a.index - b.index;
423
- })
424
- .map((entry) => entry.item);
437
+ let result = 0;
438
+ if (collator && typeof va === "string" && typeof vb === "string") result = collator.compare(va, vb);
439
+ else if (va < vb) result = -1;
440
+ else if (va > vb) result = 1;
441
+ if (result) return result * comparators[k].dir;
442
+ }
443
+ return a - b;
444
+ });
445
+ /** @type {T[]} */
446
+ const out = new Array(size);
447
+ for (let i = 0; i < size; i++) out[i] = array[indexes[i]];
448
+ return out;
425
449
  }
426
450
 
427
451
  /**
@@ -435,7 +459,9 @@ function shuffle(array) {
435
459
  const out = [...array];
436
460
  for (let i = out.length - 1; i > 0; i--) {
437
461
  const j = Math.floor(Math.random() * (i + 1));
438
- [out[i], out[j]] = [out[j], out[i]];
462
+ const item = out[i];
463
+ out[i] = out[j];
464
+ out[j] = item;
439
465
  }
440
466
  return out;
441
467
  }
@@ -463,7 +489,17 @@ function sample(array) {
463
489
  * @returns {T[]}
464
490
  */
465
491
  function sampleSize(array, n) {
466
- return shuffle(array).slice(0, Math.max(0, n));
492
+ const out = [...array];
493
+ const count = Math.min(out.length, Math.max(0, Math.floor(n)));
494
+ // A partial shuffle: only the items that are returned get moved.
495
+ for (let i = 0; i < count; i++) {
496
+ const j = i + Math.floor(Math.random() * (out.length - i));
497
+ const item = out[i];
498
+ out[i] = out[j];
499
+ out[j] = item;
500
+ }
501
+ out.length = count;
502
+ return out;
467
503
  }
468
504
 
469
505
  /**
@@ -631,7 +667,8 @@ function compact(array) {
631
667
  * @returns {T[]}
632
668
  */
633
669
  function toggle(array, item) {
634
- return array.includes(item) ? array.filter((x) => x !== item) : [...array, item];
670
+ // `includes` finds NaN, so the removal has to as well.
671
+ return array.includes(item) ? array.filter((x) => x !== item && (x === x || item === item)) : [...array, item];
635
672
  }
636
673
 
637
674
  /**
@@ -702,8 +739,7 @@ function flatten(array, depth) {
702
739
  * @returns {Array<Array<T | undefined>>}
703
740
  */
704
741
  function zip(...arrays) {
705
- const size = Math.max(0, ...arrays.map((a) => a.length));
706
- return Array.from({ length: size }, (_, i) => arrays.map((a) => a[i]));
742
+ return _zip(arrays);
707
743
  }
708
744
 
709
745
  /**
@@ -717,9 +753,28 @@ function zip(...arrays) {
717
753
  * @returns {Array<Array<T | undefined>>}
718
754
  */
719
755
  function unzip(tuples) {
720
- return zip(...tuples);
756
+ return _zip(tuples);
721
757
  }
722
758
 
759
+ /**
760
+ * @template T
761
+ * @param {ReadonlyArray<ReadonlyArray<T>>} arrays
762
+ * @returns {Array<Array<T | undefined>>}
763
+ */
764
+ const _zip = (arrays) => {
765
+ let size = 0;
766
+ for (const array of arrays) if (array.length > size) size = array.length;
767
+ /** @type {Array<Array<T | undefined>>} */
768
+ const out = new Array(size);
769
+ for (let i = 0; i < size; i++) {
770
+ /** @type {Array<T | undefined>} */
771
+ const row = new Array(arrays.length);
772
+ for (let k = 0; k < arrays.length; k++) row[k] = arrays[k][i];
773
+ out[i] = row;
774
+ }
775
+ return out;
776
+ };
777
+
723
778
  /**
724
779
  * Takes items from each array in turn, then appends the rest.
725
780
  *
@@ -733,7 +788,8 @@ function unzip(tuples) {
733
788
  function interleave(...arrays) {
734
789
  /** @type {T[]} */
735
790
  const out = [];
736
- const size = Math.max(0, ...arrays.map((a) => a.length));
791
+ let size = 0;
792
+ for (const array of arrays) if (array.length > size) size = array.length;
737
793
  for (let i = 0; i < size; i++) for (const a of arrays) if (i < a.length) out.push(a[i]);
738
794
  return out;
739
795
  }
@@ -786,7 +842,10 @@ function times(n, value) {
786
842
  */
787
843
  function sumBy(array, iteratee) {
788
844
  const fn = _fn(iteratee);
789
- return array.reduce((acc, item, i) => acc + (Number(fn(item, i)) || 0), 0);
845
+ /** @type {number[]} */
846
+ const values = new Array(array.length);
847
+ for (let i = 0; i < array.length; i++) values[i] = Number(fn(array[i], i)) || 0;
848
+ return preciseSum(values);
790
849
  }
791
850
 
792
851
  /**
@@ -821,13 +880,14 @@ function maxBy(array, iteratee) {
821
880
  let best;
822
881
  /** @type {any} */
823
882
  let bestValue;
824
- array.forEach((item, i) => {
825
- const v = fn(item, i);
826
- if (v !== null && v !== undefined && (bestValue === undefined || v > bestValue)) {
883
+ for (let i = 0; i < array.length; i++) {
884
+ const v = fn(array[i], i);
885
+ if (v === null || v === undefined || v !== v) continue;
886
+ if (bestValue === undefined || v > bestValue) {
827
887
  bestValue = v;
828
- best = item;
888
+ best = array[i];
829
889
  }
830
- });
890
+ }
831
891
  return best;
832
892
  }
833
893
 
@@ -848,16 +908,115 @@ function minBy(array, iteratee) {
848
908
  let best;
849
909
  /** @type {any} */
850
910
  let bestValue;
851
- array.forEach((item, i) => {
852
- const v = fn(item, i);
853
- if (v !== null && v !== undefined && (bestValue === undefined || v < bestValue)) {
911
+ for (let i = 0; i < array.length; i++) {
912
+ const v = fn(array[i], i);
913
+ if (v === null || v === undefined || v !== v) continue;
914
+ if (bestValue === undefined || v < bestValue) {
854
915
  bestValue = v;
855
- best = item;
916
+ best = array[i];
856
917
  }
857
- });
918
+ }
858
919
  return best;
859
920
  }
860
921
 
922
+ /**
923
+ * The items that appear more than once, each listed once.
924
+ *
925
+ * @example
926
+ * arr.duplicates([1, 2, 2, 3, 3, 3]); // [2, 3]
927
+ * arr.duplicates(users, "email"); // users whose email is taken by an earlier one
928
+ *
929
+ * @template T
930
+ * @param {readonly T[]} array
931
+ * @param {Iteratee<T>} [iteratee] What makes two items "the same". Defaults to the item itself.
932
+ * @returns {T[]} For each repeated value, its second occurrence.
933
+ */
934
+ function duplicates(array, iteratee) {
935
+ const fn = _fn(iteratee);
936
+ /** @type {Map<unknown, number>} */
937
+ const seen = new Map();
938
+ /** @type {T[]} */
939
+ const out = [];
940
+ for (let i = 0; i < array.length; i++) {
941
+ const key = fn(array[i], i);
942
+ const count = (seen.get(key) ?? 0) + 1;
943
+ seen.set(key, count);
944
+ if (count === 2) out.push(array[i]);
945
+ }
946
+ return out;
947
+ }
948
+
949
+ /**
950
+ * Items found in exactly one of the arrays, without duplicates.
951
+ *
952
+ * @example
953
+ * arr.xor([1, 2, 3], [2, 3, 4]); // [1, 4]
954
+ *
955
+ * @template T
956
+ * @param {...ReadonlyArray<T>} arrays
957
+ * @returns {T[]}
958
+ */
959
+ function xor(...arrays) {
960
+ /** @type {Map<T, number>} */
961
+ const counts = new Map();
962
+ for (const array of arrays) for (const item of new Set(array)) counts.set(item, (counts.get(item) ?? 0) + 1);
963
+ /** @type {T[]} */
964
+ const out = [];
965
+ for (const [item, count] of counts) if (count === 1) out.push(item);
966
+ return out;
967
+ }
968
+
969
+ /**
970
+ * The items from the start, up to the first one that fails the test.
971
+ *
972
+ * @example
973
+ * arr.takeWhile([1, 2, 5, 1], (n) => n < 3); // [1, 2]
974
+ *
975
+ * @template T
976
+ * @param {readonly T[]} array
977
+ * @param {(item: T, index: number) => unknown} predicate
978
+ * @returns {T[]}
979
+ */
980
+ function takeWhile(array, predicate) {
981
+ let end = 0;
982
+ while (end < array.length && predicate(array[end], end)) end++;
983
+ return array.slice(0, end);
984
+ }
985
+
986
+ /**
987
+ * Skips items from the start while they pass the test, and returns the rest.
988
+ *
989
+ * @example
990
+ * arr.dropWhile([1, 2, 5, 1], (n) => n < 3); // [5, 1]
991
+ *
992
+ * @template T
993
+ * @param {readonly T[]} array
994
+ * @param {(item: T, index: number) => unknown} predicate
995
+ * @returns {T[]}
996
+ */
997
+ function dropWhile(array, predicate) {
998
+ let start = 0;
999
+ while (start < array.length && predicate(array[start], start)) start++;
1000
+ return array.slice(start);
1001
+ }
1002
+
1003
+ /**
1004
+ * Joins items the way a sentence would, in any language.
1005
+ *
1006
+ * @example
1007
+ * arr.toSentence(["Ada", "Bob", "Cy"]); // "Ada, Bob, and Cy"
1008
+ * arr.toSentence(["Ada", "Bob"], { locale: "fr" }); // "Ada et Bob"
1009
+ * arr.toSentence(["red", "blue"], { type: "disjunction" }); // "red or blue"
1010
+ *
1011
+ * @param {Iterable<unknown>} items
1012
+ * @param {ToSentenceOptions} [options]
1013
+ * @returns {string}
1014
+ */
1015
+ function toSentence(items, options = {}) {
1016
+ const list = Array.from(items, (item) => String(item));
1017
+ return intl.list(options.locale ?? "en", { type: options.type ?? "conjunction", style: options.style ?? "long" }).format(list);
1018
+ }
1019
+
861
1020
  module.exports = {
862
1021
  chunk,
863
1022
  windows,
@@ -898,4 +1057,9 @@ module.exports = {
898
1057
  averageBy,
899
1058
  maxBy,
900
1059
  minBy,
1060
+ duplicates,
1061
+ xor,
1062
+ takeWhile,
1063
+ dropWhile,
1064
+ toSentence,
901
1065
  };