@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.
- package/CHANGELOG.md +340 -0
- package/README.md +137 -548
- package/index.js +80 -35
- package/package.json +161 -33
- package/src/Arr.js +1050 -418
- package/src/Async.js +817 -0
- package/src/Cache.js +433 -0
- package/src/Checker.js +1202 -539
- package/src/Cli.js +1293 -0
- package/src/Color.js +556 -0
- package/src/Crypto.js +1082 -0
- package/src/Emitter.js +356 -0
- package/src/Env.js +689 -0
- package/src/FS.js +1626 -894
- package/src/Func.js +823 -356
- package/src/Http.js +700 -0
- package/src/Id.js +568 -200
- package/src/Logger.js +963 -378
- package/src/Num.js +965 -360
- package/src/Obj.js +1077 -379
- package/src/SQLite.js +1629 -1005
- package/src/Schema.js +2101 -0
- package/src/Str.js +1209 -432
- package/src/Sys.js +654 -0
- package/src/Time.js +1533 -440
- package/src/Utils.js +342 -325
- package/src/errors.js +335 -0
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +735 -526
- package/types/src/Arr.d.ts +533 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +372 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +221 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +651 -232
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +664 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +300 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +620 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +189 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +412 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +717 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +508 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +444 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +199 -66
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +414 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +514 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +453 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +754 -308
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +1189 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +617 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +314 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +759 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +158 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +316 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
- 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
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
};
|