@ix-xs/node-comfort 1.2.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 (78) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +137 -548
  3. package/index.js +80 -35
  4. package/package.json +161 -33
  5. package/src/Arr.js +1050 -418
  6. package/src/Async.js +817 -0
  7. package/src/Cache.js +433 -0
  8. package/src/Checker.js +1202 -539
  9. package/src/Cli.js +1293 -0
  10. package/src/Color.js +556 -0
  11. package/src/Crypto.js +1082 -0
  12. package/src/Emitter.js +356 -0
  13. package/src/Env.js +689 -0
  14. package/src/FS.js +1626 -894
  15. package/src/Func.js +823 -356
  16. package/src/Http.js +700 -0
  17. package/src/Id.js +568 -200
  18. package/src/Logger.js +963 -378
  19. package/src/Num.js +965 -360
  20. package/src/Obj.js +1077 -379
  21. package/src/SQLite.js +1629 -1005
  22. package/src/Schema.js +2101 -0
  23. package/src/Str.js +1209 -432
  24. package/src/Sys.js +654 -0
  25. package/src/Time.js +1533 -440
  26. package/src/Utils.js +342 -325
  27. package/src/errors.js +335 -0
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +735 -526
  30. package/types/src/Arr.d.ts +533 -145
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +372 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +221 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +651 -232
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +664 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +300 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +620 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +189 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +412 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +717 -238
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +508 -135
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +444 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +199 -66
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +414 -46
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +514 -137
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +453 -104
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +754 -308
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +1189 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +617 -166
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +314 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +759 -204
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +158 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +316 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
  78. package/types/index.d.ts.map +0 -1
package/src/Arr.js CHANGED
@@ -1,433 +1,1065 @@
1
+ "use strict";
2
+
1
3
  /**
2
- * Array utilities.
4
+ * Array helpers. None of them mutate what you pass in, and item types carry
5
+ * through (`chunk(numbers, 2)` is a `number[][]`). Many take an iteratee:
6
+ * a property name, which your editor completes, or a function.
3
7
  *
4
- * Chunking, grouping, uniqueness, set operations, shuffling, flattening and
5
- * more • expressed as small pure functions that never mutate their input.
8
+ * @example
9
+ * arr.chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
10
+ * arr.groupBy(users, "role"); // { admin: [...], user: [...] }
11
+ * arr.sortBy(users, ["lastName", "age"]);
12
+ */
13
+
14
+ /**
15
+ * How to get a value out of an item: a property name, or a function
16
+ * `(item, index) => value`.
6
17
  *
7
- * Many methods accept an `iteratee`: either a property key (string) or a
8
- * function mapping an item to a value.
18
+ * @template T
19
+ * @typedef {(T extends object ? keyof T & (string | number) : never) | ((item: T, index: number) => unknown)} Iteratee
9
20
  */
10
21
 
11
22
  /**
12
- * Turns a key or function into a value-extracting function.
13
- * @private
14
- * @param {string|((item: any) => any)} [iteratee]
15
- * @returns {(item: any) => any}
23
+ * A sort key for `sortBy()`: an iteratee, or `[iteratee, "asc" | "desc"]`
24
+ * to give that key its own direction.
25
+ *
26
+ * @template T
27
+ * @typedef {Iteratee<T> | [Iteratee<T>, "asc" | "desc"]} SortKey
16
28
  */
17
- const _iteratee = (iteratee) => {
18
- if (typeof iteratee === "function") return iteratee;
19
- if (typeof iteratee === "string") return (item) => (item == null ? undefined : item[iteratee]);
29
+
30
+ /**
31
+ * Options for `sortBy()`.
32
+ * @typedef {object} SortByOptions
33
+ * @property {"asc"|"desc"} [order] Direction for keys that don't set their own. Defaults to `"asc"`.
34
+ * @property {boolean} [natural] Sort text like a person would: ignore case, respect accents, `"item2"` before `"item10"`.
35
+ * @property {string} [locale] Language used by `natural`. Defaults to the system's.
36
+ */
37
+
38
+ /**
39
+ * A page returned by `paginate()`.
40
+ * @template T
41
+ * @typedef {object} Page
42
+ * @property {T[]} items The items on this page.
43
+ * @property {number} page The page number, starting at 1.
44
+ * @property {number} perPage
45
+ * @property {number} total Total number of items.
46
+ * @property {number} pages Number of pages, at least 1.
47
+ * @property {boolean} hasPrev
48
+ * @property {boolean} hasNext
49
+ */
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
+
61
+ /**
62
+ * @param {unknown} iteratee
63
+ * @returns {(item: any, index: number) => any}
64
+ */
65
+ const _fn = (iteratee) => {
66
+ if (typeof iteratee === "function") return /** @type {(item: any, index: number) => any} */ (iteratee);
67
+ if (typeof iteratee === "string" || typeof iteratee === "number" || typeof iteratee === "symbol") {
68
+ return (item) => (item == null ? undefined : item[iteratee]);
69
+ }
20
70
  return (item) => item;
21
71
  };
22
72
 
23
- module.exports = {
24
- /**
25
- * Splits an array into chunks of a given size.
26
- *
27
- * @example
28
- * nodeComfort.arr.chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
29
- *
30
- * @param {any[]} array - Source array.
31
- * @param {number} size - Chunk size (must be >= 1).
32
- * @returns {any[][]} The chunked array.
33
- */
34
- chunk(array, size) {
35
- if (size < 1) return [];
36
- const result = [];
37
- for (let i = 0; i < array.length; i += size) {
38
- result.push(array.slice(i, i + size));
39
- }
40
- return result;
41
- },
42
-
43
- /**
44
- * Returns a new array with duplicate values removed.
45
- *
46
- * Pass an `iteratee` to de-duplicate by a derived key.
47
- *
48
- * @example
49
- * nodeComfort.arr.unique([1, 1, 2, 3]); // [1, 2, 3]
50
- * nodeComfort.arr.unique([{ id: 1 }, { id: 1 }], "id"); // [{ id: 1 }]
51
- *
52
- * @param {any[]} array - Source array.
53
- * @param {string|((item: any) => any)} [iteratee] - Key or function to compare by.
54
- * @returns {any[]} The de-duplicated array.
55
- */
56
- unique(array, iteratee) {
57
- if (!iteratee) return [...new Set(array)];
58
- const fn = _iteratee(iteratee);
59
- const seen = new Set();
60
- const result = [];
61
- for (const item of array) {
62
- const key = fn(item);
63
- if (!seen.has(key)) {
64
- seen.add(key);
65
- result.push(item);
66
- }
67
- }
68
- return result;
69
- },
70
-
71
- /**
72
- * Groups the items of an array by a key or function.
73
- *
74
- * @example
75
- * nodeComfort.arr.groupBy([1, 2, 3, 4], (n) => (n % 2 ? "odd" : "even"));
76
- * // { odd: [1, 3], even: [2, 4] }
77
- *
78
- * @param {any[]} array - Source array.
79
- * @param {string|((item: any) => any)} iteratee - Key or function to group by.
80
- * @returns {Record<string, any[]>} The grouped object.
81
- */
82
- groupBy(array, iteratee) {
83
- const fn = _iteratee(iteratee);
84
- const result = {};
85
- for (const item of array) {
86
- const key = fn(item);
87
- (result[key] ??= []).push(item);
88
- }
89
- return result;
90
- },
91
-
92
- /**
93
- * Builds an object keyed by a derived value, mapping each key to the
94
- * (last) matching item.
95
- *
96
- * @example
97
- * nodeComfort.arr.keyBy([{ id: "a" }, { id: "b" }], "id");
98
- * // { a: { id: "a" }, b: { id: "b" } }
99
- *
100
- * @param {any[]} array - Source array.
101
- * @param {string|((item: any) => any)} iteratee - Key or function.
102
- * @returns {Record<string, any>} The keyed object.
103
- */
104
- keyBy(array, iteratee) {
105
- const fn = _iteratee(iteratee);
106
- const result = {};
107
- for (const item of array) result[fn(item)] = item;
108
- return result;
109
- },
110
-
111
- /**
112
- * Partitions an array into two groups based on a predicate:
113
- * `[matching, notMatching]`.
114
- *
115
- * @example
116
- * nodeComfort.arr.partition([1, 2, 3, 4], (n) => n % 2 === 0);
117
- * // [[2, 4], [1, 3]]
118
- *
119
- * @param {any[]} array - Source array.
120
- * @param {(item: any, index: number) => boolean} predicate - Test function.
121
- * @returns {[any[], any[]]} The two partitions.
122
- */
123
- partition(array, predicate) {
124
- const pass = [];
125
- const fail = [];
126
- array.forEach((item, i) => (predicate(item, i) ? pass : fail).push(item));
127
- return [pass, fail];
128
- },
129
-
130
- /**
131
- * Recursively flattens a nested array up to a given depth.
132
- *
133
- * @example
134
- * nodeComfort.arr.flatten([1, [2, [3, [4]]]]); // [1, 2, 3, [4]]
135
- * nodeComfort.arr.flatten([1, [2, [3]]], Infinity); // [1, 2, 3]
136
- *
137
- * @param {any[]} array - Source array.
138
- * @param {number} [depth=1] - Maximum depth to flatten.
139
- * @returns {any[]} The flattened array.
140
- */
141
- flatten(array, depth = 1) {
142
- return array.flat(depth);
143
- },
144
-
145
- /**
146
- * Returns a shuffled copy of an array (Fisher–Yates).
147
- *
148
- * Uses `Math.random` • not cryptographically secure.
149
- *
150
- * @param {any[]} array - Source array.
151
- * @returns {any[]} A new shuffled array.
152
- */
153
- shuffle(array) {
154
- const result = [...array];
155
- for (let i = result.length - 1; i > 0; i--) {
156
- const j = Math.floor(Math.random() * (i + 1));
157
- [result[i], result[j]] = [result[j], result[i]];
158
- }
159
- return result;
160
- },
161
-
162
- /**
163
- * Returns a random element from an array.
164
- *
165
- * @param {any[]} array - Source array.
166
- * @returns {any} A random element, or undefined if empty.
167
- */
168
- sample(array) {
169
- return array.length ? array[Math.floor(Math.random() * array.length)] : undefined;
170
- },
171
-
172
- /**
173
- * Returns `n` random elements from an array (without repetition).
174
- *
175
- * @param {any[]} array - Source array.
176
- * @param {number} n - Number of elements to pick.
177
- * @returns {any[]} The sampled elements.
178
- */
179
- sampleSize(array, n) {
180
- return this.shuffle(array).slice(0, Math.max(0, n));
181
- },
182
-
183
- /**
184
- * Sorts an array by one or more iteratees, ascending by default.
185
- *
186
- * Does not mutate the input.
187
- *
188
- * @example
189
- * nodeComfort.arr.sortBy(users, "age");
190
- * nodeComfort.arr.sortBy(users, [(u) => u.age, "name"]);
191
- *
192
- * @param {any[]} array - Source array.
193
- * @param {string|((item: any) => any)|Array<string|((item: any) => any)>} iteratees - Sort key(s).
194
- * @param {"asc"|"desc"} [direction="asc"] - Sort direction.
195
- * @returns {any[]} A new sorted array.
196
- */
197
- sortBy(array, iteratees, direction = "asc") {
198
- const fns = (Array.isArray(iteratees) ? iteratees : [iteratees]).map(_iteratee);
199
- const dir = direction === "desc" ? -1 : 1;
200
- return [...array].sort((a, b) => {
201
- for (const fn of fns) {
202
- const va = fn(a);
203
- const vb = fn(b);
204
- if (va < vb) return -1 * dir;
205
- if (va > vb) return 1 * dir;
73
+ /**
74
+ * @param {unknown} key
75
+ * @returns {PropertyKey}
76
+ */
77
+ const _key = (key) => (typeof key === "symbol" || typeof key === "number" ? key : String(key));
78
+
79
+ /**
80
+ * Splits an array into groups of `size` items. The last group can be shorter.
81
+ *
82
+ * @example
83
+ * arr.chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
84
+ * arr.chunk(emails, 100).forEach(sendBatch);
85
+ *
86
+ * @template T
87
+ * @param {readonly T[]} array
88
+ * @param {number} size
89
+ * @returns {T[][]}
90
+ */
91
+ function chunk(array, size) {
92
+ const n = Math.floor(size);
93
+ if (!(n >= 1)) return [];
94
+ /** @type {T[][]} */
95
+ const out = [];
96
+ for (let i = 0; i < array.length; i += n) out.push(array.slice(i, i + n));
97
+ return out;
98
+ }
99
+
100
+ /**
101
+ * Sliding windows of `size` items, moving by `step`. Good for moving
102
+ * averages and comparing neighbours.
103
+ *
104
+ * @example
105
+ * arr.windows([1, 2, 3, 4], 2); // [[1, 2], [2, 3], [3, 4]]
106
+ * arr.windows([1, 2, 3, 4, 5], 3, 2); // [[1, 2, 3], [3, 4, 5]]
107
+ *
108
+ * @template T
109
+ * @param {readonly T[]} array
110
+ * @param {number} size
111
+ * @param {number} [step=1]
112
+ * @returns {T[][]}
113
+ */
114
+ function windows(array, size, step = 1) {
115
+ const n = Math.floor(size);
116
+ const s = Math.max(1, Math.floor(step));
117
+ /** @type {T[][]} */
118
+ const out = [];
119
+ if (!(n >= 1)) return out;
120
+ for (let i = 0; i + n <= array.length; i += s) out.push(array.slice(i, i + n));
121
+ return out;
122
+ }
123
+
124
+ /**
125
+ * The first item, or the first `n` items.
126
+ *
127
+ * @example
128
+ * arr.first([1, 2, 3]); // 1
129
+ * arr.first([1, 2, 3], 2); // [1, 2]
130
+ *
131
+ * @template T
132
+ * @overload
133
+ * @param {readonly T[]} array
134
+ * @returns {T | undefined}
135
+ */
136
+ /**
137
+ * The first `n` items.
138
+ *
139
+ * @template T
140
+ * @overload
141
+ * @param {readonly T[]} array
142
+ * @param {number} n
143
+ * @returns {T[]}
144
+ */
145
+ /**
146
+ * @template T
147
+ * @param {readonly T[]} array
148
+ * @param {number} [n]
149
+ * @returns {T | T[] | undefined}
150
+ */
151
+ function first(array, n) {
152
+ if (n === undefined) return array[0];
153
+ return array.slice(0, Math.max(0, n));
154
+ }
155
+
156
+ /**
157
+ * The last item, or the last `n` items.
158
+ *
159
+ * @example
160
+ * arr.last([1, 2, 3]); // 3
161
+ * arr.last([1, 2, 3], 2); // [2, 3]
162
+ *
163
+ * @template T
164
+ * @overload
165
+ * @param {readonly T[]} array
166
+ * @returns {T | undefined}
167
+ */
168
+ /**
169
+ * The last `n` items.
170
+ *
171
+ * @template T
172
+ * @overload
173
+ * @param {readonly T[]} array
174
+ * @param {number} n
175
+ * @returns {T[]}
176
+ */
177
+ /**
178
+ * @template T
179
+ * @param {readonly T[]} array
180
+ * @param {number} [n]
181
+ * @returns {T | T[] | undefined}
182
+ */
183
+ function last(array, n) {
184
+ if (n === undefined) return array[array.length - 1];
185
+ return n <= 0 ? [] : array.slice(-n);
186
+ }
187
+
188
+ /**
189
+ * One page of an array, with what you need to draw pagination controls.
190
+ * Pages start at 1; an out-of-range page is brought back into range.
191
+ *
192
+ * @example
193
+ * arr.paginate(products, 2, 20);
194
+ * // { items: [...], page: 2, perPage: 20, total: 95, pages: 5, hasPrev: true, hasNext: true }
195
+ *
196
+ * @template T
197
+ * @param {readonly T[]} array
198
+ * @param {number} [page=1]
199
+ * @param {number} [perPage=20]
200
+ * @returns {Page<T>}
201
+ */
202
+ function paginate(array, page = 1, perPage = 20) {
203
+ const size = Math.max(1, Math.floor(perPage));
204
+ const pages = Math.max(1, Math.ceil(array.length / size));
205
+ const current = Math.min(Math.max(1, Math.floor(page) || 1), pages);
206
+ const start = (current - 1) * size;
207
+ return {
208
+ items: array.slice(start, start + size),
209
+ page: current,
210
+ perPage: size,
211
+ total: array.length,
212
+ pages,
213
+ hasPrev: current > 1,
214
+ hasNext: current < pages,
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Removes duplicates. With an iteratee, items that give the same value are
220
+ * duplicates; the first one is kept.
221
+ *
222
+ * @example
223
+ * arr.unique([1, 1, 2, 3]); // [1, 2, 3]
224
+ * arr.unique(users, "email"); // one user per email
225
+ * arr.unique(tags, (t) => t.toLowerCase());
226
+ *
227
+ * @template T
228
+ * @param {readonly T[]} array
229
+ * @param {Iteratee<T>} [iteratee]
230
+ * @returns {T[]}
231
+ */
232
+ function unique(array, iteratee) {
233
+ if (iteratee === undefined) return [...new Set(array)];
234
+ const fn = _fn(iteratee);
235
+ const seen = new Set();
236
+ return array.filter((item, i) => {
237
+ const key = fn(item, i);
238
+ if (seen.has(key)) return false;
239
+ seen.add(key);
240
+ return true;
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Groups items by key.
246
+ *
247
+ * @example
248
+ * arr.groupBy(users, "role"); // { admin: [...], user: [...] }
249
+ * arr.groupBy([1, 2, 3, 4], (n) => (n % 2 ? "odd" : "even")); // { odd: [1, 3], even: [2, 4] }
250
+ *
251
+ * @template T
252
+ * @param {readonly T[]} array
253
+ * @param {Iteratee<T>} iteratee
254
+ * @returns {Record<string, T[]>}
255
+ */
256
+ function groupBy(array, iteratee) {
257
+ const fn = _fn(iteratee);
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);
268
+ }
269
+
270
+ /**
271
+ * Indexes items by key. If two items share a key, the last one wins.
272
+ *
273
+ * @example
274
+ * const byId = arr.keyBy(users, "id");
275
+ * byId[42]; // user 42
276
+ *
277
+ * @template T
278
+ * @param {readonly T[]} array
279
+ * @param {Iteratee<T>} iteratee
280
+ * @returns {Record<string, T>}
281
+ */
282
+ function keyBy(array, iteratee) {
283
+ const fn = _fn(iteratee);
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);
288
+ }
289
+
290
+ /**
291
+ * Builds a `Map` from an array. Unlike `keyBy()`, keys keep their type.
292
+ *
293
+ * @example
294
+ * arr.toMap(users, "id"); // Map<number, User>
295
+ * arr.toMap(users, "id", (u) => u.name); // Map<number, string>
296
+ *
297
+ * @template T
298
+ * @template [V=T]
299
+ * @param {readonly T[]} array
300
+ * @param {Iteratee<T>} key
301
+ * @param {(item: T, index: number) => V} [value] The value to store. Defaults to the item.
302
+ * @returns {Map<any, V>}
303
+ */
304
+ function toMap(array, key, value) {
305
+ const keyFn = _fn(key);
306
+ /** @type {Map<any, V>} */
307
+ const map = new Map();
308
+ array.forEach((item, i) => map.set(keyFn(item, i), value ? value(item, i) : /** @type {V} */ (/** @type {unknown} */ (item))));
309
+ return map;
310
+ }
311
+
312
+ /**
313
+ * Counts items per key.
314
+ *
315
+ * @example
316
+ * arr.countBy(["a", "b", "a"]); // { a: 2, b: 1 }
317
+ * arr.countBy(users, "country"); // { FR: 12, US: 7 }
318
+ *
319
+ * @template T
320
+ * @param {readonly T[]} array
321
+ * @param {Iteratee<T>} [iteratee] Defaults to the item itself.
322
+ * @returns {Record<string, number>}
323
+ */
324
+ function countBy(array, iteratee) {
325
+ const fn = _fn(iteratee);
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);
333
+ }
334
+
335
+ /**
336
+ * Counts the items that pass a test.
337
+ *
338
+ * @example
339
+ * arr.count(users, (u) => u.active);
340
+ *
341
+ * @template T
342
+ * @param {readonly T[]} array
343
+ * @param {(item: T, index: number) => unknown} predicate
344
+ * @returns {number}
345
+ */
346
+ function count(array, predicate) {
347
+ let total = 0;
348
+ array.forEach((item, i) => {
349
+ if (predicate(item, i)) total++;
350
+ });
351
+ return total;
352
+ }
353
+
354
+ /**
355
+ * Splits items into those that pass a test and those that don't.
356
+ *
357
+ * @example
358
+ * const [adults, minors] = arr.partition(people, (p) => p.age >= 18);
359
+ *
360
+ * @template T
361
+ * @param {readonly T[]} array
362
+ * @param {(item: T, index: number) => unknown} predicate
363
+ * @returns {[T[], T[]]}
364
+ */
365
+ function partition(array, predicate) {
366
+ /** @type {T[]} */
367
+ const pass = [];
368
+ /** @type {T[]} */
369
+ const fail = [];
370
+ array.forEach((item, i) => (predicate(item, i) ? pass : fail).push(item));
371
+ return [pass, fail];
372
+ }
373
+
374
+ /**
375
+ * Takes one property from every item.
376
+ *
377
+ * @example
378
+ * arr.pluck(users, "email"); // ["ada@x.io", "bob@x.io"]
379
+ *
380
+ * @template T
381
+ * @template {keyof T} K
382
+ * @param {readonly T[]} array
383
+ * @param {K} key
384
+ * @returns {Array<T[K]>}
385
+ */
386
+ function pluck(array, key) {
387
+ return array.map((item) => (item == null ? /** @type {T[K]} */ (/** @type {unknown} */ (undefined)) : item[key]));
388
+ }
389
+
390
+ /**
391
+ * Sorts by one or more keys, each with its own direction if you like.
392
+ * The sort is stable and `null`/`undefined` always go last.
393
+ *
394
+ * @example
395
+ * arr.sortBy(users, "age"); // youngest first
396
+ * arr.sortBy(users, "age", "desc"); // oldest first
397
+ * arr.sortBy(users, [["age", "desc"], "lastName"]);
398
+ * arr.sortBy(files, "name", { natural: true }); // "file2" before "file10"
399
+ *
400
+ * @template T
401
+ * @param {readonly T[]} array
402
+ * @param {SortKey<T> | Array<SortKey<T>>} keys
403
+ * @param {"asc" | "desc" | SortByOptions} [order="asc"]
404
+ * @returns {T[]}
405
+ */
406
+ function sortBy(array, keys, order = "asc") {
407
+ const options = typeof order === "string" ? { order } : order;
408
+ const defaultDir = options.order === "desc" ? -1 : 1;
409
+ const collator = options.natural ? intl.collator(options.locale, { numeric: true, sensitivity: "base" }) : null;
410
+ const isTuple = (/** @type {unknown} */ k) => Array.isArray(k) && k.length === 2 && (k[1] === "asc" || k[1] === "desc");
411
+ const list = Array.isArray(keys) && !isTuple(keys) ? keys : [keys];
412
+ const comparators = list.map((k) => {
413
+ const [iteratee, dir] = isTuple(k) ? /** @type {[unknown, string]} */ (k) : [k, undefined];
414
+ return { fn: _fn(iteratee), dir: dir === undefined ? defaultDir : dir === "desc" ? -1 : 1 };
415
+ });
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;
206
436
  }
207
- return 0;
208
- });
209
- },
210
-
211
- /**
212
- * Returns the elements present in the first array but not in the others.
213
- *
214
- * @example
215
- * nodeComfort.arr.difference([1, 2, 3, 4], [2, 4]); // [1, 3]
216
- *
217
- * @param {any[]} array - Source array.
218
- * @param {...any[]} others - Arrays to subtract.
219
- * @returns {any[]} The difference.
220
- */
221
- difference(array, ...others) {
222
- const exclude = new Set(others.flat());
223
- return array.filter((item) => !exclude.has(item));
224
- },
225
-
226
- /**
227
- * Returns the elements common to all provided arrays.
228
- *
229
- * @example
230
- * nodeComfort.arr.intersection([1, 2, 3], [2, 3, 4]); // [2, 3]
231
- *
232
- * @param {...any[]} arrays - Arrays to intersect.
233
- * @returns {any[]} The intersection.
234
- */
235
- intersection(...arrays) {
236
- if (!arrays.length) return [];
237
- const [first, ...rest] = arrays;
238
- const sets = rest.map((a) => new Set(a));
239
- return [...new Set(first)].filter((item) => sets.every((set) => set.has(item)));
240
- },
241
-
242
- /**
243
- * Returns the union (unique values) of all provided arrays.
244
- *
245
- * @example
246
- * nodeComfort.arr.union([1, 2], [2, 3], [3, 4]); // [1, 2, 3, 4]
247
- *
248
- * @param {...any[]} arrays - Arrays to unite.
249
- * @returns {any[]} The union.
250
- */
251
- union(...arrays) {
252
- return [...new Set(arrays.flat())];
253
- },
254
-
255
- /**
256
- * Zips multiple arrays together into an array of tuples.
257
- *
258
- * @example
259
- * nodeComfort.arr.zip(["a", "b"], [1, 2]); // [["a", 1], ["b", 2]]
260
- *
261
- * @param {...any[]} arrays - Arrays to zip.
262
- * @returns {any[][]} The zipped tuples.
263
- */
264
- zip(...arrays) {
265
- const length = Math.max(0, ...arrays.map((a) => a.length));
266
- const result = [];
267
- for (let i = 0; i < length; i++) {
268
- result.push(arrays.map((a) => a[i]));
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;
269
442
  }
270
- return result;
271
- },
272
-
273
- /**
274
- * Splits an array into `matching` and removes falsy values.
275
- *
276
- * Removes `false`, `null`, `0`, `""`, `undefined` and `NaN`.
277
- *
278
- * @example
279
- * nodeComfort.arr.compact([0, 1, false, 2, "", 3, null]); // [1, 2, 3]
280
- *
281
- * @param {any[]} array - Source array.
282
- * @returns {any[]} The compacted array.
283
- */
284
- compact(array) {
285
- return array.filter(Boolean);
286
- },
287
-
288
- /**
289
- * Returns the first element (or the first `n` elements) of an array.
290
- *
291
- * @param {any[]} array - Source array.
292
- * @param {number} [n] - How many elements to take.
293
- * @returns {any} The first element, or the first `n` elements when `n` is given.
294
- */
295
- first(array, n) {
296
- if (n === undefined) return array[0];
297
- return array.slice(0, Math.max(0, n));
298
- },
299
-
300
- /**
301
- * Returns the last element (or the last `n` elements) of an array.
302
- *
303
- * @param {any[]} array - Source array.
304
- * @param {number} [n] - How many elements to take.
305
- * @returns {any} The last element, or the last `n` elements when `n` is given.
306
- */
307
- last(array, n) {
308
- if (n === undefined) return array[array.length - 1];
309
- return array.slice(Math.max(0, array.length - n));
310
- },
311
-
312
- /**
313
- * Removes items from an array where the predicate is true, returning a new array.
314
- *
315
- * @param {any[]} array - Source array.
316
- * @param {(item: any, index: number) => boolean} predicate - Removal test.
317
- * @returns {any[]} A new array without the removed items.
318
- */
319
- remove(array, predicate) {
320
- return array.filter((item, i) => !predicate(item, i));
321
- },
322
-
323
- /**
324
- * Moves an element from one index to another (returns a new array).
325
- *
326
- * @param {any[]} array - Source array.
327
- * @param {number} from - Source index.
328
- * @param {number} to - Destination index.
329
- * @returns {any[]} The reordered array.
330
- */
331
- move(array, from, to) {
332
- const result = [...array];
333
- const [item] = result.splice(from, 1);
334
- result.splice(to, 0, item);
335
- return result;
336
- },
337
-
338
- /**
339
- * Counts occurrences of each derived key in an array.
340
- *
341
- * @example
342
- * nodeComfort.arr.countBy(["a", "b", "a"]); // { a: 2, b: 1 }
343
- * nodeComfort.arr.countBy([1.1, 2.3, 1.7], Math.floor); // { 1: 2, 2: 1 }
344
- *
345
- * @param {any[]} array - Source array.
346
- * @param {string|((item: any) => any)} [iteratee] - Key or function.
347
- * @returns {Record<string, number>} The counts.
348
- */
349
- countBy(array, iteratee) {
350
- const fn = _iteratee(iteratee);
351
- const result = {};
352
- for (const item of array) {
353
- const key = fn(item);
354
- result[key] = (result[key] ?? 0) + 1;
355
- }
356
- return result;
357
- },
358
-
359
- /**
360
- * Sums an array by a numeric iteratee (or the values themselves).
361
- *
362
- * @example
363
- * nodeComfort.arr.sumBy([{ n: 1 }, { n: 2 }], "n"); // 3
364
- *
365
- * @param {any[]} array - Source array.
366
- * @param {string|((item: any) => number)} [iteratee] - Key or function returning a number.
367
- * @returns {number} The sum.
368
- */
369
- sumBy(array, iteratee) {
370
- const fn = _iteratee(iteratee);
371
- return array.reduce((acc, item) => acc + (Number(fn(item)) || 0), 0);
372
- },
373
-
374
- /**
375
- * Returns the item with the maximum derived value.
376
- *
377
- * @param {any[]} array - Source array.
378
- * @param {string|((item: any) => number)} [iteratee] - Key or function.
379
- * @returns {any} The item, or undefined if the array is empty.
380
- */
381
- maxBy(array, iteratee) {
382
- const fn = _iteratee(iteratee);
383
- let best;
384
- let bestVal = -Infinity;
385
- for (const item of array) {
386
- const val = fn(item);
387
- if (val > bestVal) {
388
- bestVal = val;
389
- best = item;
390
- }
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;
449
+ }
450
+
451
+ /**
452
+ * A shuffled copy (Fisher-Yates).
453
+ *
454
+ * @template T
455
+ * @param {readonly T[]} array
456
+ * @returns {T[]}
457
+ */
458
+ function shuffle(array) {
459
+ const out = [...array];
460
+ for (let i = out.length - 1; i > 0; i--) {
461
+ const j = Math.floor(Math.random() * (i + 1));
462
+ const item = out[i];
463
+ out[i] = out[j];
464
+ out[j] = item;
465
+ }
466
+ return out;
467
+ }
468
+
469
+ /**
470
+ * One random item, or `undefined` if the array is empty.
471
+ *
472
+ * @template T
473
+ * @param {readonly T[]} array
474
+ * @returns {T | undefined}
475
+ */
476
+ function sample(array) {
477
+ return array.length ? array[Math.floor(Math.random() * array.length)] : undefined;
478
+ }
479
+
480
+ /**
481
+ * `n` random items, never the same one twice.
482
+ *
483
+ * @example
484
+ * arr.sampleSize(players, 3);
485
+ *
486
+ * @template T
487
+ * @param {readonly T[]} array
488
+ * @param {number} n
489
+ * @returns {T[]}
490
+ */
491
+ function sampleSize(array, 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;
503
+ }
504
+
505
+ /**
506
+ * Rotates items. Positive `n` moves items from the start to the end,
507
+ * negative `n` the other way.
508
+ *
509
+ * @example
510
+ * arr.rotate([1, 2, 3, 4], 1); // [2, 3, 4, 1]
511
+ * arr.rotate([1, 2, 3, 4], -1); // [4, 1, 2, 3]
512
+ *
513
+ * @template T
514
+ * @param {readonly T[]} array
515
+ * @param {number} [n=1]
516
+ * @returns {T[]}
517
+ */
518
+ function rotate(array, n = 1) {
519
+ if (!array.length) return [];
520
+ const k = ((Math.trunc(n) % array.length) + array.length) % array.length;
521
+ return [...array.slice(k), ...array.slice(0, k)];
522
+ }
523
+
524
+ /**
525
+ * Moves one item to another position. Negative indexes count from the end.
526
+ *
527
+ * @example
528
+ * arr.move(["a", "b", "c"], 0, 2); // ["b", "c", "a"]
529
+ *
530
+ * @template T
531
+ * @param {readonly T[]} array
532
+ * @param {number} from
533
+ * @param {number} to
534
+ * @returns {T[]}
535
+ */
536
+ function move(array, from, to) {
537
+ const out = [...array];
538
+ const len = out.length;
539
+ const src = from < 0 ? len + from : from;
540
+ if (src < 0 || src >= len) return out;
541
+ const [item] = out.splice(src, 1);
542
+ out.splice(to < 0 ? len + to : to, 0, item);
543
+ return out;
544
+ }
545
+
546
+ /**
547
+ * Swaps two items.
548
+ *
549
+ * @example
550
+ * arr.swap(["a", "b", "c"], 0, 2); // ["c", "b", "a"]
551
+ *
552
+ * @template T
553
+ * @param {readonly T[]} array
554
+ * @param {number} i
555
+ * @param {number} j
556
+ * @returns {T[]}
557
+ */
558
+ function swap(array, i, j) {
559
+ const out = [...array];
560
+ if (i in out && j in out) [out[i], out[j]] = [out[j], out[i]];
561
+ return out;
562
+ }
563
+
564
+ /**
565
+ * Items of `array` that appear in none of the others.
566
+ *
567
+ * @example
568
+ * arr.difference([1, 2, 3, 4], [2, 4]); // [1, 3]
569
+ *
570
+ * @template T
571
+ * @param {readonly T[]} array
572
+ * @param {...ReadonlyArray<T>} others
573
+ * @returns {T[]}
574
+ */
575
+ function difference(array, ...others) {
576
+ const exclude = new Set(others.flat());
577
+ return array.filter((item) => !exclude.has(item));
578
+ }
579
+
580
+ /**
581
+ * Items present in every array, without duplicates.
582
+ *
583
+ * @example
584
+ * arr.intersection([1, 2, 3], [2, 3, 4], [3, 2]); // [2, 3]
585
+ *
586
+ * @template T
587
+ * @param {...ReadonlyArray<T>} arrays
588
+ * @returns {T[]}
589
+ */
590
+ function intersection(...arrays) {
591
+ if (!arrays.length) return [];
592
+ const [head, ...rest] = arrays;
593
+ const sets = rest.map((a) => new Set(a));
594
+ return [...new Set(head)].filter((item) => sets.every((set) => set.has(item)));
595
+ }
596
+
597
+ /**
598
+ * Items present in any array, without duplicates.
599
+ *
600
+ * @example
601
+ * arr.union([1, 2], [2, 3], [3, 4]); // [1, 2, 3, 4]
602
+ *
603
+ * @template T
604
+ * @param {...ReadonlyArray<T>} arrays
605
+ * @returns {T[]}
606
+ */
607
+ function union(...arrays) {
608
+ return [...new Set(arrays.flat())];
609
+ }
610
+
611
+ /**
612
+ * A copy without the given values. `NaN` works too.
613
+ *
614
+ * @example
615
+ * arr.without([1, 2, 3, 2], 2); // [1, 3]
616
+ *
617
+ * @template T
618
+ * @param {readonly T[]} array
619
+ * @param {...T} values
620
+ * @returns {T[]}
621
+ */
622
+ function without(array, ...values) {
623
+ const exclude = new Set(values);
624
+ return array.filter((item) => !exclude.has(item));
625
+ }
626
+
627
+ /**
628
+ * A copy without the items that pass the test.
629
+ *
630
+ * @example
631
+ * arr.remove([1, 2, 3, 4], (n) => n % 2 === 0); // [1, 3]
632
+ *
633
+ * @template T
634
+ * @param {readonly T[]} array
635
+ * @param {(item: T, index: number) => unknown} predicate
636
+ * @returns {T[]}
637
+ */
638
+ function remove(array, predicate) {
639
+ return array.filter((item, i) => !predicate(item, i));
640
+ }
641
+
642
+ /**
643
+ * Removes falsy values (`false`, `null`, `undefined`, `0`, `""`, `NaN`).
644
+ *
645
+ * @example
646
+ * arr.compact([0, 1, false, 2, "", 3, null]); // [1, 2, 3]
647
+ *
648
+ * @template T
649
+ * @param {readonly T[]} array
650
+ * @returns {Array<Exclude<T, false | 0 | 0n | "" | null | undefined>>}
651
+ */
652
+ function compact(array) {
653
+ return /** @type {any} */ (array.filter(Boolean));
654
+ }
655
+
656
+ /**
657
+ * Adds the item if it's missing, removes it if it's there. Handy for
658
+ * multi-select state.
659
+ *
660
+ * @example
661
+ * arr.toggle(["a", "b"], "b"); // ["a"]
662
+ * arr.toggle(["a"], "b"); // ["a", "b"]
663
+ *
664
+ * @template T
665
+ * @param {readonly T[]} array
666
+ * @param {T} item
667
+ * @returns {T[]}
668
+ */
669
+ function toggle(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];
672
+ }
673
+
674
+ /**
675
+ * Replaces the item with the same identity, or appends it if there's none.
676
+ *
677
+ * @example
678
+ * arr.upsert(users, { id: 2, name: "Bob" }, "id");
679
+ *
680
+ * @template T
681
+ * @param {readonly T[]} array
682
+ * @param {T} item
683
+ * @param {Iteratee<T>} identity What makes two items "the same", like `"id"`.
684
+ * @returns {T[]}
685
+ */
686
+ function upsert(array, item, identity) {
687
+ const fn = _fn(identity);
688
+ const id = fn(item, -1);
689
+ const index = array.findIndex((x, i) => fn(x, i) === id);
690
+ if (index === -1) return [...array, item];
691
+ const out = [...array];
692
+ out[index] = item;
693
+ return out;
694
+ }
695
+
696
+ /**
697
+ * Inserts items at an index. Negative indexes count from the end.
698
+ *
699
+ * @example
700
+ * arr.insert(["a", "d"], 1, "b", "c"); // ["a", "b", "c", "d"]
701
+ *
702
+ * @template T
703
+ * @param {readonly T[]} array
704
+ * @param {number} index
705
+ * @param {...T} items
706
+ * @returns {T[]}
707
+ */
708
+ function insert(array, index, ...items) {
709
+ const out = [...array];
710
+ out.splice(index, 0, ...items);
711
+ return out;
712
+ }
713
+
714
+ /**
715
+ * Flattens nested arrays, one level by default.
716
+ *
717
+ * @example
718
+ * arr.flatten([1, [2, [3, [4]]]]); // [1, 2, [3, [4]]]
719
+ * arr.flatten([1, [2, [3, [4]]]], Infinity); // [1, 2, 3, 4]
720
+ *
721
+ * @template T
722
+ * @template {number} [D=1]
723
+ * @param {readonly T[]} array
724
+ * @param {D} [depth]
725
+ * @returns {FlatArray<T[], D>[]}
726
+ */
727
+ function flatten(array, depth) {
728
+ return /** @type {any} */ (array.flat(depth ?? 1));
729
+ }
730
+
731
+ /**
732
+ * Pairs items by position. Shorter arrays leave `undefined` holes.
733
+ *
734
+ * @example
735
+ * arr.zip(["a", "b"], [1, 2]); // [["a", 1], ["b", 2]]
736
+ *
737
+ * @template T
738
+ * @param {...ReadonlyArray<T>} arrays
739
+ * @returns {Array<Array<T | undefined>>}
740
+ */
741
+ function zip(...arrays) {
742
+ return _zip(arrays);
743
+ }
744
+
745
+ /**
746
+ * The opposite of `zip()`: rows become columns.
747
+ *
748
+ * @example
749
+ * arr.unzip([["a", 1], ["b", 2]]); // [["a", "b"], [1, 2]]
750
+ *
751
+ * @template T
752
+ * @param {ReadonlyArray<ReadonlyArray<T>>} tuples
753
+ * @returns {Array<Array<T | undefined>>}
754
+ */
755
+ function unzip(tuples) {
756
+ return _zip(tuples);
757
+ }
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
+
778
+ /**
779
+ * Takes items from each array in turn, then appends the rest.
780
+ *
781
+ * @example
782
+ * arr.interleave([1, 3, 5], [2, 4]); // [1, 2, 3, 4, 5]
783
+ *
784
+ * @template T
785
+ * @param {...ReadonlyArray<T>} arrays
786
+ * @returns {T[]}
787
+ */
788
+ function interleave(...arrays) {
789
+ /** @type {T[]} */
790
+ const out = [];
791
+ let size = 0;
792
+ for (const array of arrays) if (array.length > size) size = array.length;
793
+ for (let i = 0; i < size; i++) for (const a of arrays) if (i < a.length) out.push(a[i]);
794
+ return out;
795
+ }
796
+
797
+ /**
798
+ * Every combination of one item from each array.
799
+ *
800
+ * @example
801
+ * arr.cartesian(["S", "M"], ["red", "blue"]);
802
+ * // [["S", "red"], ["S", "blue"], ["M", "red"], ["M", "blue"]]
803
+ *
804
+ * @template T
805
+ * @param {...ReadonlyArray<T>} arrays
806
+ * @returns {T[][]}
807
+ */
808
+ function cartesian(...arrays) {
809
+ if (!arrays.length) return [];
810
+ return arrays.reduce((acc, list) => acc.flatMap((combo) => list.map((item) => [...combo, item])), /** @type {T[][]} */ ([[]]));
811
+ }
812
+
813
+ /**
814
+ * An array of `n` items, from a value or a function of the index.
815
+ *
816
+ * @example
817
+ * arr.times(3, "x"); // ["x", "x", "x"]
818
+ * arr.times(3, (i) => i * 2); // [0, 2, 4]
819
+ *
820
+ * @template T
821
+ * @param {number} n
822
+ * @param {T | ((index: number) => T)} [value]
823
+ * @returns {T[]}
824
+ */
825
+ function times(n, value) {
826
+ const size = Math.max(0, Math.floor(n));
827
+ const fn = typeof value === "function" ? /** @type {(index: number) => T} */ (value) : () => /** @type {T} */ (value);
828
+ return Array.from({ length: size }, (_, i) => fn(i));
829
+ }
830
+
831
+ /**
832
+ * Adds up a number from each item. Non-numbers count as 0.
833
+ *
834
+ * @example
835
+ * arr.sumBy(cart, "price");
836
+ * arr.sumBy(cart, (item) => item.price * item.quantity);
837
+ *
838
+ * @template T
839
+ * @param {readonly T[]} array
840
+ * @param {Iteratee<T>} [iteratee]
841
+ * @returns {number}
842
+ */
843
+ function sumBy(array, iteratee) {
844
+ const fn = _fn(iteratee);
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);
849
+ }
850
+
851
+ /**
852
+ * The average of a number from each item, or `0` for an empty array.
853
+ *
854
+ * @example
855
+ * arr.averageBy(reviews, "rating"); // 4.3
856
+ *
857
+ * @template T
858
+ * @param {readonly T[]} array
859
+ * @param {Iteratee<T>} [iteratee]
860
+ * @returns {number}
861
+ */
862
+ function averageBy(array, iteratee) {
863
+ return array.length ? sumBy(array, iteratee) / array.length : 0;
864
+ }
865
+
866
+ /**
867
+ * The item with the highest value.
868
+ *
869
+ * @example
870
+ * arr.maxBy(players, "score");
871
+ *
872
+ * @template T
873
+ * @param {readonly T[]} array
874
+ * @param {Iteratee<T>} [iteratee]
875
+ * @returns {T | undefined}
876
+ */
877
+ function maxBy(array, iteratee) {
878
+ const fn = _fn(iteratee);
879
+ /** @type {T | undefined} */
880
+ let best;
881
+ /** @type {any} */
882
+ let 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) {
887
+ bestValue = v;
888
+ best = array[i];
391
889
  }
392
- return best;
393
- },
394
-
395
- /**
396
- * Returns the item with the minimum derived value.
397
- *
398
- * @param {any[]} array - Source array.
399
- * @param {string|((item: any) => number)} [iteratee] - Key or function.
400
- * @returns {any} The item, or undefined if the array is empty.
401
- */
402
- minBy(array, iteratee) {
403
- const fn = _iteratee(iteratee);
404
- let best;
405
- let bestVal = Infinity;
406
- for (const item of array) {
407
- const val = fn(item);
408
- if (val < bestVal) {
409
- bestVal = val;
410
- best = item;
411
- }
890
+ }
891
+ return best;
892
+ }
893
+
894
+ /**
895
+ * The item with the lowest value.
896
+ *
897
+ * @example
898
+ * arr.minBy(products, "price");
899
+ *
900
+ * @template T
901
+ * @param {readonly T[]} array
902
+ * @param {Iteratee<T>} [iteratee]
903
+ * @returns {T | undefined}
904
+ */
905
+ function minBy(array, iteratee) {
906
+ const fn = _fn(iteratee);
907
+ /** @type {T | undefined} */
908
+ let best;
909
+ /** @type {any} */
910
+ let 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) {
915
+ bestValue = v;
916
+ best = array[i];
412
917
  }
413
- return best;
414
- },
415
-
416
- /**
417
- * Creates an array of a given length, filled by a value or generator function.
418
- *
419
- * @example
420
- * nodeComfort.arr.times(3, "x"); // ["x", "x", "x"]
421
- * nodeComfort.arr.times(3, (i) => i * 2); // [0, 2, 4]
422
- *
423
- * @param {number} n - Number of items.
424
- * @param {any|((index: number) => any)} [value] - Fill value or generator.
425
- * @returns {any[]} The generated array.
426
- */
427
- times(n, value) {
428
- const fn = typeof value === "function" ? value : () => value;
429
- const result = [];
430
- for (let i = 0; i < n; i++) result.push(fn(i));
431
- return result;
432
- },
918
+ }
919
+ return best;
920
+ }
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
+
1020
+ module.exports = {
1021
+ chunk,
1022
+ windows,
1023
+ first,
1024
+ last,
1025
+ paginate,
1026
+ unique,
1027
+ groupBy,
1028
+ keyBy,
1029
+ toMap,
1030
+ countBy,
1031
+ count,
1032
+ partition,
1033
+ pluck,
1034
+ sortBy,
1035
+ shuffle,
1036
+ sample,
1037
+ sampleSize,
1038
+ rotate,
1039
+ move,
1040
+ swap,
1041
+ difference,
1042
+ intersection,
1043
+ union,
1044
+ without,
1045
+ remove,
1046
+ compact,
1047
+ toggle,
1048
+ upsert,
1049
+ insert,
1050
+ flatten,
1051
+ zip,
1052
+ unzip,
1053
+ interleave,
1054
+ cartesian,
1055
+ times,
1056
+ sumBy,
1057
+ averageBy,
1058
+ maxBy,
1059
+ minBy,
1060
+ duplicates,
1061
+ xor,
1062
+ takeWhile,
1063
+ dropWhile,
1064
+ toSentence,
433
1065
  };