@cooljs123/easyjs 0.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/LICENSE +15 -0
- package/README.md +602 -0
- package/package.json +20 -0
- package/src/easy.js +1457 -0
- package/src/index.js +3 -0
package/src/easy.js
ADDED
|
@@ -0,0 +1,1457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the input unchanged.
|
|
3
|
+
* @template T
|
|
4
|
+
* @param {T} x
|
|
5
|
+
* @returns {T}
|
|
6
|
+
*/
|
|
7
|
+
function identity(x) {
|
|
8
|
+
return x;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Do nothing.
|
|
13
|
+
* @returns {void}
|
|
14
|
+
*/
|
|
15
|
+
function noop() {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check for null or undefined.
|
|
19
|
+
* @param {*} v
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
function isNil(v) {
|
|
23
|
+
return v === null || v === undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if a value is "empty" (null/undefined, empty string/array/object/map/set).
|
|
28
|
+
* @param {*} v
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
function isEmpty(v) {
|
|
32
|
+
if (isNil(v)) return true;
|
|
33
|
+
if (typeof v === "string") return v.length === 0;
|
|
34
|
+
if (Array.isArray(v)) return v.length === 0;
|
|
35
|
+
if (v instanceof Map || v instanceof Set) return v.size === 0;
|
|
36
|
+
if (typeof v === "object") return Object.keys(v).length === 0;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Return fallback if value is null/undefined.
|
|
42
|
+
* @template T
|
|
43
|
+
* @param {T} v
|
|
44
|
+
* @param {T} fallback
|
|
45
|
+
* @returns {T}
|
|
46
|
+
*/
|
|
47
|
+
function defaultTo(v, fallback) {
|
|
48
|
+
return isNil(v) ? fallback : v;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Convert to lowercase string.
|
|
53
|
+
* @param {*} s
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
function toLower(s) {
|
|
57
|
+
return String(s).toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Convert to uppercase string.
|
|
62
|
+
* @param {*} s
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
function toUpper(s) {
|
|
66
|
+
return String(s).toUpperCase();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Capitalize first letter.
|
|
71
|
+
* @param {*} s
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
function capitalize(s) {
|
|
75
|
+
const str = String(s);
|
|
76
|
+
return str.length ? str[0].toUpperCase() + str.slice(1) : "";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Trim whitespace.
|
|
81
|
+
* @param {*} s
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
function trim(s) {
|
|
85
|
+
return String(s).trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Lowercase + trim.
|
|
90
|
+
* @param {*} s
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
function normalizeString(s) {
|
|
94
|
+
return trim(toLower(s));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Slugify a string (lowercase, hyphenated).
|
|
99
|
+
* @param {*} s
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
function slugify(s) {
|
|
103
|
+
return String(s)
|
|
104
|
+
.toLowerCase()
|
|
105
|
+
.trim()
|
|
106
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
107
|
+
.replace(/^-+|-+$/g, "");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Left pad a string.
|
|
112
|
+
* @param {*} s
|
|
113
|
+
* @param {number} length
|
|
114
|
+
* @param {string} [padChar=" "]
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
function padLeft(s, length, padChar = " ") {
|
|
118
|
+
return String(s).padStart(length, padChar);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Right pad a string.
|
|
123
|
+
* @param {*} s
|
|
124
|
+
* @param {number} length
|
|
125
|
+
* @param {string} [padChar=" "]
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
function padRight(s, length, padChar = " ") {
|
|
129
|
+
return String(s).padEnd(length, padChar);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Repeat a string n times.
|
|
134
|
+
* @param {*} s
|
|
135
|
+
* @param {number} times
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
function repeat(s, times) {
|
|
139
|
+
return String(s).repeat(times);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Remove non-alphanumeric characters.
|
|
144
|
+
* @param {*} s
|
|
145
|
+
* @returns {string}
|
|
146
|
+
*/
|
|
147
|
+
function stripNonAlpha(s) {
|
|
148
|
+
return String(s).replace(/[^a-z0-9]+/gi, "");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if string is blank (empty or whitespace).
|
|
153
|
+
* @param {*} s
|
|
154
|
+
* @returns {boolean}
|
|
155
|
+
*/
|
|
156
|
+
function isBlank(s) {
|
|
157
|
+
return String(s).trim().length === 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Wrap non-array value in array; null/undefined -> [].
|
|
162
|
+
* @param {*} v
|
|
163
|
+
* @returns {Array}
|
|
164
|
+
*/
|
|
165
|
+
function asArray(v) {
|
|
166
|
+
if (isNil(v)) return [];
|
|
167
|
+
return Array.isArray(v) ? v : [v];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function arrayFromArgs(args, fallback) {
|
|
171
|
+
if (args.length === 0) return fallback || [];
|
|
172
|
+
if (Array.isArray(args[0])) return args[0];
|
|
173
|
+
return Array.prototype.slice.call(args);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check if value is in a list of candidates.
|
|
178
|
+
* @param {*} value
|
|
179
|
+
* @param {Array|*} candidates
|
|
180
|
+
* @returns {boolean}
|
|
181
|
+
*/
|
|
182
|
+
function anyOf(value, candidates) {
|
|
183
|
+
const list = Array.isArray(candidates) ? candidates : Array.prototype.slice.call(arguments, 1);
|
|
184
|
+
return list.includes(value);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Alias for anyOf.
|
|
189
|
+
* @param {*} value
|
|
190
|
+
* @param {Array|*} candidates
|
|
191
|
+
* @returns {boolean}
|
|
192
|
+
*/
|
|
193
|
+
function eqAny(value, candidates) {
|
|
194
|
+
return anyOf.apply(null, arguments);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check if value is included in array.
|
|
199
|
+
* @param {*} value
|
|
200
|
+
* @param {Array} arr
|
|
201
|
+
* @returns {boolean}
|
|
202
|
+
*/
|
|
203
|
+
function inArray(value, arr) {
|
|
204
|
+
if (Array.isArray(arr)) return arr.includes(value);
|
|
205
|
+
if (arguments.length > 2) {
|
|
206
|
+
const list = Array.prototype.slice.call(arguments, 1);
|
|
207
|
+
return list.includes(value);
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Clamp a number to a range.
|
|
214
|
+
* @param {*} n
|
|
215
|
+
* @param {number} min
|
|
216
|
+
* @param {number} max
|
|
217
|
+
* @returns {number}
|
|
218
|
+
*/
|
|
219
|
+
function clamp(n, min, max) {
|
|
220
|
+
const num = Number(n);
|
|
221
|
+
return Math.min(max, Math.max(min, num));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check if a number is between min and max.
|
|
226
|
+
* @param {*} n
|
|
227
|
+
* @param {number} min
|
|
228
|
+
* @param {number} max
|
|
229
|
+
* @param {boolean} [inclusive=true]
|
|
230
|
+
* @returns {boolean}
|
|
231
|
+
*/
|
|
232
|
+
function between(n, min, max, inclusive = true) {
|
|
233
|
+
const num = Number(n);
|
|
234
|
+
return inclusive ? num >= min && num <= max : num > min && num < max;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Parse integer with fallback to 0.
|
|
239
|
+
* @param {*} v
|
|
240
|
+
* @param {number} [base=10]
|
|
241
|
+
* @returns {number}
|
|
242
|
+
*/
|
|
243
|
+
function toInt(v, base = 10) {
|
|
244
|
+
const n = parseInt(v, base);
|
|
245
|
+
return Number.isNaN(n) ? 0 : n;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Parse float with fallback to 0.
|
|
250
|
+
* @param {*} v
|
|
251
|
+
* @returns {number}
|
|
252
|
+
*/
|
|
253
|
+
function toFloat(v) {
|
|
254
|
+
const n = parseFloat(v);
|
|
255
|
+
return Number.isNaN(n) ? 0 : n;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Convert to number with fallback 0.
|
|
260
|
+
* @param {*} v
|
|
261
|
+
* @returns {number}
|
|
262
|
+
*/
|
|
263
|
+
function toNumber(v) {
|
|
264
|
+
const n = Number(v);
|
|
265
|
+
return Number.isNaN(n) ? 0 : n;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Check if value is a finite number.
|
|
270
|
+
* @param {*} v
|
|
271
|
+
* @returns {boolean}
|
|
272
|
+
*/
|
|
273
|
+
function isNumber(v) {
|
|
274
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Check if value is an integer.
|
|
279
|
+
* @param {*} v
|
|
280
|
+
* @returns {boolean}
|
|
281
|
+
*/
|
|
282
|
+
function isInt(v) {
|
|
283
|
+
return Number.isInteger(v);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Filter an array by predicate or remove null/undefined.
|
|
288
|
+
* @param {Array} arr
|
|
289
|
+
* @param {Function} [predicate]
|
|
290
|
+
* @returns {Array}
|
|
291
|
+
*/
|
|
292
|
+
function compact(arr, predicate) {
|
|
293
|
+
if (!Array.isArray(arr)) {
|
|
294
|
+
const args = Array.prototype.slice.call(arguments);
|
|
295
|
+
const last = args[args.length - 1];
|
|
296
|
+
if (typeof last === "function") {
|
|
297
|
+
predicate = last;
|
|
298
|
+
arr = args.slice(0, -1);
|
|
299
|
+
} else {
|
|
300
|
+
arr = args;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (typeof predicate === "function") return arr.filter(predicate);
|
|
304
|
+
return arr.filter(v => !isNil(v));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Unique values from array.
|
|
309
|
+
* @param {Array} arr
|
|
310
|
+
* @returns {Array}
|
|
311
|
+
*/
|
|
312
|
+
function uniq(arr) {
|
|
313
|
+
if (!Array.isArray(arr)) arr = arrayFromArgs(arguments, []);
|
|
314
|
+
return Array.from(new Set(arr));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Flatten one level.
|
|
319
|
+
* @param {Array} arr
|
|
320
|
+
* @returns {Array}
|
|
321
|
+
*/
|
|
322
|
+
function flatten(arr) {
|
|
323
|
+
if (!Array.isArray(arr)) arr = arrayFromArgs(arguments, []);
|
|
324
|
+
return arr.reduce((acc, v) => acc.concat(v), []);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Split array into chunks.
|
|
329
|
+
* @param {Array} arr
|
|
330
|
+
* @param {number} size
|
|
331
|
+
* @returns {Array<Array>}
|
|
332
|
+
*/
|
|
333
|
+
function chunk(arr, size) {
|
|
334
|
+
if (!Array.isArray(arr)) {
|
|
335
|
+
const args = Array.prototype.slice.call(arguments);
|
|
336
|
+
size = args.pop();
|
|
337
|
+
arr = args;
|
|
338
|
+
}
|
|
339
|
+
if (!Array.isArray(arr) || size <= 0) return [];
|
|
340
|
+
const out = [];
|
|
341
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
342
|
+
out.push(arr.slice(i, i + size));
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Create a numeric range (end exclusive).
|
|
349
|
+
* @param {number} start
|
|
350
|
+
* @param {number} [end]
|
|
351
|
+
* @param {number} [step=1]
|
|
352
|
+
* @returns {Array<number>}
|
|
353
|
+
*/
|
|
354
|
+
function range(start, end, step = 1) {
|
|
355
|
+
let s = start;
|
|
356
|
+
let e = end;
|
|
357
|
+
if (end === undefined) {
|
|
358
|
+
s = 0;
|
|
359
|
+
e = start;
|
|
360
|
+
}
|
|
361
|
+
if (step === 0) return [];
|
|
362
|
+
const out = [];
|
|
363
|
+
const inc = step > 0;
|
|
364
|
+
for (let i = s; inc ? i < e : i > e; i += step) {
|
|
365
|
+
out.push(i);
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Minimum of array or args.
|
|
372
|
+
* @param {Array|number} arr
|
|
373
|
+
* @returns {number}
|
|
374
|
+
*/
|
|
375
|
+
function min(arr) {
|
|
376
|
+
if (!Array.isArray(arr)) arr = arrayFromArgs(arguments, []);
|
|
377
|
+
if (arr.length === 0) return 0;
|
|
378
|
+
return Math.min.apply(null, arr.map(Number));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Maximum of array or args.
|
|
383
|
+
* @param {Array|number} arr
|
|
384
|
+
* @returns {number}
|
|
385
|
+
*/
|
|
386
|
+
function max(arr) {
|
|
387
|
+
if (!Array.isArray(arr)) arr = arrayFromArgs(arguments, []);
|
|
388
|
+
if (arr.length === 0) return 0;
|
|
389
|
+
return Math.max.apply(null, arr.map(Number));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Sum array values.
|
|
394
|
+
* @param {Array} arr
|
|
395
|
+
* @returns {number}
|
|
396
|
+
*/
|
|
397
|
+
function sum(arr) {
|
|
398
|
+
if (!Array.isArray(arr)) arr = arrayFromArgs(arguments, []);
|
|
399
|
+
return arr.reduce((a, b) => a + Number(b || 0), 0);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Average array values.
|
|
404
|
+
* @param {Array} arr
|
|
405
|
+
* @returns {number}
|
|
406
|
+
*/
|
|
407
|
+
function avg(arr) {
|
|
408
|
+
if (!Array.isArray(arr)) arr = arrayFromArgs(arguments, []);
|
|
409
|
+
if (arr.length === 0) return 0;
|
|
410
|
+
return sum(arr) / arr.length;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Median of array or args.
|
|
415
|
+
* @param {Array|number} arr
|
|
416
|
+
* @returns {number}
|
|
417
|
+
*/
|
|
418
|
+
function median(arr) {
|
|
419
|
+
if (!Array.isArray(arr)) arr = arrayFromArgs(arguments, []);
|
|
420
|
+
if (arr.length === 0) return 0;
|
|
421
|
+
const sorted = arr.map(Number).slice().sort((a, b) => a - b);
|
|
422
|
+
const mid = Math.floor(sorted.length / 2);
|
|
423
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Mode (most frequent) of array or args.
|
|
428
|
+
* @param {Array|number} arr
|
|
429
|
+
* @returns {number}
|
|
430
|
+
*/
|
|
431
|
+
function mode(arr) {
|
|
432
|
+
if (!Array.isArray(arr)) arr = arrayFromArgs(arguments, []);
|
|
433
|
+
if (arr.length === 0) return 0;
|
|
434
|
+
const counts = new Map();
|
|
435
|
+
let best = arr[0];
|
|
436
|
+
let bestCount = 0;
|
|
437
|
+
for (const v of arr) {
|
|
438
|
+
const c = (counts.get(v) || 0) + 1;
|
|
439
|
+
counts.set(v, c);
|
|
440
|
+
if (c > bestCount) {
|
|
441
|
+
bestCount = c;
|
|
442
|
+
best = v;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return Number(best);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Percentile (0-100) of array or args.
|
|
450
|
+
* @param {Array|number} arr
|
|
451
|
+
* @param {number} p
|
|
452
|
+
* @returns {number}
|
|
453
|
+
*/
|
|
454
|
+
function percentile(arr, p) {
|
|
455
|
+
if (!Array.isArray(arr)) {
|
|
456
|
+
const args = Array.prototype.slice.call(arguments);
|
|
457
|
+
p = args.pop();
|
|
458
|
+
arr = args;
|
|
459
|
+
}
|
|
460
|
+
if (!Array.isArray(arr) || arr.length === 0) return 0;
|
|
461
|
+
const sorted = arr.map(Number).slice().sort((a, b) => a - b);
|
|
462
|
+
const idx = Math.min(sorted.length - 1, Math.max(0, Math.round((p / 100) * (sorted.length - 1))));
|
|
463
|
+
return sorted[idx];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Group by key or key function.
|
|
468
|
+
* @param {Array} arr
|
|
469
|
+
* @param {Function|string} keyFn
|
|
470
|
+
* @returns {Object}
|
|
471
|
+
*/
|
|
472
|
+
function groupBy(arr, keyFn) {
|
|
473
|
+
if (!Array.isArray(arr)) {
|
|
474
|
+
const args = Array.prototype.slice.call(arguments);
|
|
475
|
+
keyFn = args.pop();
|
|
476
|
+
arr = args;
|
|
477
|
+
}
|
|
478
|
+
if (!Array.isArray(arr)) return {};
|
|
479
|
+
const fn = typeof keyFn === "function" ? keyFn : (x) => x[keyFn];
|
|
480
|
+
return arr.reduce((acc, item) => {
|
|
481
|
+
const key = fn(item);
|
|
482
|
+
if (!acc[key]) acc[key] = [];
|
|
483
|
+
acc[key].push(item);
|
|
484
|
+
return acc;
|
|
485
|
+
}, {});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* First element of array.
|
|
490
|
+
* @param {Array} arr
|
|
491
|
+
* @returns {*}
|
|
492
|
+
*/
|
|
493
|
+
function first(arr) {
|
|
494
|
+
if (!Array.isArray(arr)) arr = arrayFromArgs(arguments, []);
|
|
495
|
+
return arr[0];
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Last element of array.
|
|
500
|
+
* @param {Array} arr
|
|
501
|
+
* @returns {*}
|
|
502
|
+
*/
|
|
503
|
+
function last(arr) {
|
|
504
|
+
if (!Array.isArray(arr)) arr = arrayFromArgs(arguments, []);
|
|
505
|
+
return arr[arr.length - 1];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Take first n items.
|
|
510
|
+
* @param {Array} arr
|
|
511
|
+
* @param {number} n
|
|
512
|
+
* @returns {Array}
|
|
513
|
+
*/
|
|
514
|
+
function take(arr, n) {
|
|
515
|
+
if (!Array.isArray(arr)) {
|
|
516
|
+
const args = Array.prototype.slice.call(arguments);
|
|
517
|
+
n = args.pop();
|
|
518
|
+
arr = args;
|
|
519
|
+
}
|
|
520
|
+
return Array.isArray(arr) ? arr.slice(0, n) : [];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Take last n items.
|
|
525
|
+
* @param {Array} arr
|
|
526
|
+
* @param {number} n
|
|
527
|
+
* @returns {Array}
|
|
528
|
+
*/
|
|
529
|
+
function takeRight(arr, n) {
|
|
530
|
+
if (!Array.isArray(arr)) {
|
|
531
|
+
const args = Array.prototype.slice.call(arguments);
|
|
532
|
+
n = args.pop();
|
|
533
|
+
arr = args;
|
|
534
|
+
}
|
|
535
|
+
return Array.isArray(arr) ? arr.slice(Math.max(0, arr.length - n)) : [];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Drop first n items.
|
|
540
|
+
* @param {Array} arr
|
|
541
|
+
* @param {number} n
|
|
542
|
+
* @returns {Array}
|
|
543
|
+
*/
|
|
544
|
+
function drop(arr, n) {
|
|
545
|
+
if (!Array.isArray(arr)) {
|
|
546
|
+
const args = Array.prototype.slice.call(arguments);
|
|
547
|
+
n = args.pop();
|
|
548
|
+
arr = args;
|
|
549
|
+
}
|
|
550
|
+
return Array.isArray(arr) ? arr.slice(n) : [];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Drop last n items.
|
|
555
|
+
* @param {Array} arr
|
|
556
|
+
* @param {number} n
|
|
557
|
+
* @returns {Array}
|
|
558
|
+
*/
|
|
559
|
+
function dropRight(arr, n) {
|
|
560
|
+
if (!Array.isArray(arr)) {
|
|
561
|
+
const args = Array.prototype.slice.call(arguments);
|
|
562
|
+
n = args.pop();
|
|
563
|
+
arr = args;
|
|
564
|
+
}
|
|
565
|
+
return Array.isArray(arr) ? arr.slice(0, Math.max(0, arr.length - n)) : [];
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Zip arrays into array of tuples.
|
|
570
|
+
* @param {Array} arrs
|
|
571
|
+
* @returns {Array<Array>}
|
|
572
|
+
*/
|
|
573
|
+
function zip(arrs) {
|
|
574
|
+
const lists = Array.isArray(arrs) && Array.isArray(arrs[0]) ? arrs : Array.prototype.slice.call(arguments);
|
|
575
|
+
if (lists.length === 0) return [];
|
|
576
|
+
const minLen = Math.min.apply(null, lists.map(a => a.length));
|
|
577
|
+
const out = [];
|
|
578
|
+
for (let i = 0; i < minLen; i++) {
|
|
579
|
+
out.push(lists.map(a => a[i]));
|
|
580
|
+
}
|
|
581
|
+
return out;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Unzip array of tuples.
|
|
586
|
+
* @param {Array<Array>} arr
|
|
587
|
+
* @returns {Array<Array>}
|
|
588
|
+
*/
|
|
589
|
+
function unzip(arr) {
|
|
590
|
+
if (!Array.isArray(arr) || arr.length === 0) return [];
|
|
591
|
+
return zip.apply(null, arr);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Intersection of arrays.
|
|
596
|
+
* @param {Array} arr
|
|
597
|
+
* @returns {Array}
|
|
598
|
+
*/
|
|
599
|
+
function intersection(arr) {
|
|
600
|
+
const lists = Array.isArray(arr) && Array.isArray(arr[0]) ? arr : Array.prototype.slice.call(arguments);
|
|
601
|
+
if (lists.length === 0) return [];
|
|
602
|
+
return lists.reduce((acc, cur) => acc.filter(x => cur.includes(x)));
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Difference of arrays (first minus rest).
|
|
607
|
+
* @param {Array} arr
|
|
608
|
+
* @returns {Array}
|
|
609
|
+
*/
|
|
610
|
+
function difference(arr) {
|
|
611
|
+
const lists = Array.isArray(arr) && Array.isArray(arr[0]) ? arr : Array.prototype.slice.call(arguments);
|
|
612
|
+
if (lists.length === 0) return [];
|
|
613
|
+
const [firstArr, ...rest] = lists;
|
|
614
|
+
const exclude = new Set(flatten(rest));
|
|
615
|
+
return firstArr.filter(x => !exclude.has(x));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Union of arrays.
|
|
620
|
+
* @param {Array} arr
|
|
621
|
+
* @returns {Array}
|
|
622
|
+
*/
|
|
623
|
+
function union(arr) {
|
|
624
|
+
const lists = Array.isArray(arr) && Array.isArray(arr[0]) ? arr : Array.prototype.slice.call(arguments);
|
|
625
|
+
return uniq(flatten(lists));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Pick keys from an object.
|
|
630
|
+
* @param {Object} obj
|
|
631
|
+
* @param {Array|string} keys
|
|
632
|
+
* @returns {Object}
|
|
633
|
+
*/
|
|
634
|
+
function pick(obj, keys) {
|
|
635
|
+
if (!obj || typeof obj !== "object") return {};
|
|
636
|
+
const list = Array.isArray(keys) ? keys : Array.prototype.slice.call(arguments, 1);
|
|
637
|
+
const out = {};
|
|
638
|
+
list.forEach(k => {
|
|
639
|
+
if (k in obj) out[k] = obj[k];
|
|
640
|
+
});
|
|
641
|
+
return out;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Omit keys from an object.
|
|
646
|
+
* @param {Object} obj
|
|
647
|
+
* @param {Array|string} keys
|
|
648
|
+
* @returns {Object}
|
|
649
|
+
*/
|
|
650
|
+
function omit(obj, keys) {
|
|
651
|
+
if (!obj || typeof obj !== "object") return {};
|
|
652
|
+
const list = new Set(Array.isArray(keys) ? keys : Array.prototype.slice.call(arguments, 1));
|
|
653
|
+
const out = {};
|
|
654
|
+
Object.keys(obj).forEach(k => {
|
|
655
|
+
if (!list.has(k)) out[k] = obj[k];
|
|
656
|
+
});
|
|
657
|
+
return out;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Check if object has own property.
|
|
662
|
+
* @param {Object} obj
|
|
663
|
+
* @param {string} key
|
|
664
|
+
* @returns {boolean}
|
|
665
|
+
*/
|
|
666
|
+
function has(obj, key) {
|
|
667
|
+
return !!obj && Object.prototype.hasOwnProperty.call(obj, key);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Object.keys shortcut.
|
|
672
|
+
* @param {Object} obj
|
|
673
|
+
* @returns {Array<string>}
|
|
674
|
+
*/
|
|
675
|
+
function keys(obj) {
|
|
676
|
+
return obj ? Object.keys(obj) : [];
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Object.values shortcut.
|
|
681
|
+
* @param {Object} obj
|
|
682
|
+
* @returns {Array}
|
|
683
|
+
*/
|
|
684
|
+
function values(obj) {
|
|
685
|
+
return obj ? Object.values(obj) : [];
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Object.entries shortcut.
|
|
690
|
+
* @param {Object} obj
|
|
691
|
+
* @returns {Array<Array>}
|
|
692
|
+
*/
|
|
693
|
+
function entries(obj) {
|
|
694
|
+
return obj ? Object.entries(obj) : [];
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Object.fromEntries shortcut.
|
|
699
|
+
* @param {Array<Array>} arr
|
|
700
|
+
* @returns {Object}
|
|
701
|
+
*/
|
|
702
|
+
function fromEntries(arr) {
|
|
703
|
+
return Object.fromEntries(Array.isArray(arr) ? arr : []);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Shallow merge objects (right wins).
|
|
708
|
+
* @param {Object} obj
|
|
709
|
+
* @returns {Object}
|
|
710
|
+
*/
|
|
711
|
+
function merge(obj) {
|
|
712
|
+
const list = Array.isArray(obj) && !Array.isArray(obj[0]) ? obj : Array.prototype.slice.call(arguments);
|
|
713
|
+
return Object.assign({}, ...list);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Deep merge objects (plain objects only).
|
|
718
|
+
* @param {Object} obj
|
|
719
|
+
* @returns {Object}
|
|
720
|
+
*/
|
|
721
|
+
function deepMerge(obj) {
|
|
722
|
+
const list = Array.prototype.slice.call(arguments);
|
|
723
|
+
const isPlain = (v) => v && typeof v === "object" && !Array.isArray(v);
|
|
724
|
+
const out = {};
|
|
725
|
+
list.forEach(src => {
|
|
726
|
+
if (!isPlain(src)) return;
|
|
727
|
+
Object.keys(src).forEach(k => {
|
|
728
|
+
if (isPlain(src[k]) && isPlain(out[k])) {
|
|
729
|
+
out[k] = deepMerge(out[k], src[k]);
|
|
730
|
+
} else {
|
|
731
|
+
out[k] = src[k];
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
return out;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Check if value is a plain object.
|
|
740
|
+
* @param {*} v
|
|
741
|
+
* @returns {boolean}
|
|
742
|
+
*/
|
|
743
|
+
function isObject(v) {
|
|
744
|
+
return v && typeof v === "object" && !Array.isArray(v);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Size of array/string/object/map/set.
|
|
749
|
+
* @param {*} v
|
|
750
|
+
* @returns {number}
|
|
751
|
+
*/
|
|
752
|
+
function size(v) {
|
|
753
|
+
if (isNil(v)) return 0;
|
|
754
|
+
if (typeof v === "string" || Array.isArray(v)) return v.length;
|
|
755
|
+
if (v instanceof Map || v instanceof Set) return v.size;
|
|
756
|
+
if (typeof v === "object") return Object.keys(v).length;
|
|
757
|
+
return 0;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Normalize a path to array form.
|
|
762
|
+
* @param {string|Array} path
|
|
763
|
+
* @returns {Array<string>}
|
|
764
|
+
*/
|
|
765
|
+
function parsePath(path) {
|
|
766
|
+
if (Array.isArray(path)) return path;
|
|
767
|
+
return String(path)
|
|
768
|
+
.replace(/\[(\d+)\]/g, ".$1")
|
|
769
|
+
.split(".")
|
|
770
|
+
.filter(Boolean);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Safe get by path with fallback.
|
|
775
|
+
* @param {Object} obj
|
|
776
|
+
* @param {string|Array} path
|
|
777
|
+
* @param {*} fallback
|
|
778
|
+
* @returns {*}
|
|
779
|
+
*/
|
|
780
|
+
function safeGet(obj, path, fallback) {
|
|
781
|
+
const parts = parsePath(path);
|
|
782
|
+
let cur = obj;
|
|
783
|
+
for (let i = 0; i < parts.length; i++) {
|
|
784
|
+
if (cur == null) return fallback;
|
|
785
|
+
cur = cur[parts[i]];
|
|
786
|
+
}
|
|
787
|
+
return cur === undefined ? fallback : cur;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Safe set by path (creates objects).
|
|
792
|
+
* @param {Object} obj
|
|
793
|
+
* @param {string|Array} path
|
|
794
|
+
* @param {*} value
|
|
795
|
+
* @returns {Object}
|
|
796
|
+
*/
|
|
797
|
+
function safeSet(obj, path, value) {
|
|
798
|
+
const parts = parsePath(path);
|
|
799
|
+
if (!parts.length) return obj;
|
|
800
|
+
let cur = obj;
|
|
801
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
802
|
+
const key = parts[i];
|
|
803
|
+
if (cur[key] == null || typeof cur[key] !== "object") cur[key] = {};
|
|
804
|
+
cur = cur[key];
|
|
805
|
+
}
|
|
806
|
+
cur[parts[parts.length - 1]] = value;
|
|
807
|
+
return obj;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Try/catch helper with fallback value or handler.
|
|
812
|
+
* @param {Function} fn
|
|
813
|
+
* @param {*|Function} fallback
|
|
814
|
+
* @returns {*}
|
|
815
|
+
*/
|
|
816
|
+
function tryCatch(fn, fallback) {
|
|
817
|
+
try {
|
|
818
|
+
return fn();
|
|
819
|
+
} catch (err) {
|
|
820
|
+
return typeof fallback === "function" ? fallback(err) : fallback;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Run a side-effect function and return the original value.
|
|
826
|
+
* @param {*} value
|
|
827
|
+
* @param {Function} fn
|
|
828
|
+
* @returns {*}
|
|
829
|
+
*/
|
|
830
|
+
function tap(value, fn) {
|
|
831
|
+
if (typeof fn === "function") fn(value);
|
|
832
|
+
return value;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Sleep for ms.
|
|
837
|
+
* @param {number} ms
|
|
838
|
+
* @returns {Promise<void>}
|
|
839
|
+
*/
|
|
840
|
+
function sleep(ms) {
|
|
841
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Check if string includes any of the candidates.
|
|
846
|
+
* @param {*} str
|
|
847
|
+
* @param {Array|string} candidates
|
|
848
|
+
* @returns {boolean}
|
|
849
|
+
*/
|
|
850
|
+
function containsAny(str, candidates) {
|
|
851
|
+
const s = String(str);
|
|
852
|
+
const list = Array.isArray(candidates) ? candidates : Array.prototype.slice.call(arguments, 1);
|
|
853
|
+
return list.some(c => s.includes(String(c)));
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Check if string starts with any of the candidates.
|
|
858
|
+
* @param {*} str
|
|
859
|
+
* @param {Array|string} candidates
|
|
860
|
+
* @returns {boolean}
|
|
861
|
+
*/
|
|
862
|
+
function startsWithAny(str, candidates) {
|
|
863
|
+
const s = String(str);
|
|
864
|
+
const list = Array.isArray(candidates) ? candidates : Array.prototype.slice.call(arguments, 1);
|
|
865
|
+
return list.some(c => s.startsWith(String(c)));
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Check if string ends with any of the candidates.
|
|
870
|
+
* @param {*} str
|
|
871
|
+
* @param {Array|string} candidates
|
|
872
|
+
* @returns {boolean}
|
|
873
|
+
*/
|
|
874
|
+
function endsWithAny(str, candidates) {
|
|
875
|
+
const s = String(str);
|
|
876
|
+
const list = Array.isArray(candidates) ? candidates : Array.prototype.slice.call(arguments, 1);
|
|
877
|
+
return list.some(c => s.endsWith(String(c)));
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Safe JSON parse with fallback.
|
|
882
|
+
* @param {string} s
|
|
883
|
+
* @param {*} fallback
|
|
884
|
+
* @returns {*}
|
|
885
|
+
*/
|
|
886
|
+
function safeJsonParse(s, fallback) {
|
|
887
|
+
try {
|
|
888
|
+
return JSON.parse(s);
|
|
889
|
+
} catch (err) {
|
|
890
|
+
return fallback;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Run a function only once.
|
|
896
|
+
* @param {Function} fn
|
|
897
|
+
* @returns {Function}
|
|
898
|
+
*/
|
|
899
|
+
function once(fn) {
|
|
900
|
+
let called = false;
|
|
901
|
+
let value;
|
|
902
|
+
return function wrapped() {
|
|
903
|
+
if (!called) {
|
|
904
|
+
called = true;
|
|
905
|
+
value = fn.apply(this, arguments);
|
|
906
|
+
}
|
|
907
|
+
return value;
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Debounce a function.
|
|
913
|
+
* @param {Function} fn
|
|
914
|
+
* @param {number} wait
|
|
915
|
+
* @returns {Function}
|
|
916
|
+
*/
|
|
917
|
+
function debounce(fn, wait) {
|
|
918
|
+
let timer = null;
|
|
919
|
+
return function debounced() {
|
|
920
|
+
const args = arguments;
|
|
921
|
+
const ctx = this;
|
|
922
|
+
clearTimeout(timer);
|
|
923
|
+
timer = setTimeout(() => fn.apply(ctx, args), wait);
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Throttle a function.
|
|
929
|
+
* @param {Function} fn
|
|
930
|
+
* @param {number} wait
|
|
931
|
+
* @returns {Function}
|
|
932
|
+
*/
|
|
933
|
+
function throttle(fn, wait) {
|
|
934
|
+
let last = 0;
|
|
935
|
+
let timer = null;
|
|
936
|
+
return function throttled() {
|
|
937
|
+
const now = Date.now();
|
|
938
|
+
const remaining = wait - (now - last);
|
|
939
|
+
const ctx = this;
|
|
940
|
+
const args = arguments;
|
|
941
|
+
if (remaining <= 0) {
|
|
942
|
+
if (timer) {
|
|
943
|
+
clearTimeout(timer);
|
|
944
|
+
timer = null;
|
|
945
|
+
}
|
|
946
|
+
last = now;
|
|
947
|
+
fn.apply(ctx, args);
|
|
948
|
+
} else if (!timer) {
|
|
949
|
+
timer = setTimeout(() => {
|
|
950
|
+
last = Date.now();
|
|
951
|
+
timer = null;
|
|
952
|
+
fn.apply(ctx, args);
|
|
953
|
+
}, remaining);
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Unique by key or key function.
|
|
960
|
+
* @param {Array} arr
|
|
961
|
+
* @param {Function|string} keyFn
|
|
962
|
+
* @returns {Array}
|
|
963
|
+
*/
|
|
964
|
+
function uniqueBy(arr, keyFn) {
|
|
965
|
+
if (!Array.isArray(arr)) {
|
|
966
|
+
const args = Array.prototype.slice.call(arguments);
|
|
967
|
+
keyFn = args.pop();
|
|
968
|
+
arr = args;
|
|
969
|
+
}
|
|
970
|
+
if (!Array.isArray(arr)) return [];
|
|
971
|
+
const fn = typeof keyFn === "function" ? keyFn : (x) => x[keyFn];
|
|
972
|
+
const seen = new Set();
|
|
973
|
+
const out = [];
|
|
974
|
+
for (const item of arr) {
|
|
975
|
+
const key = fn(item);
|
|
976
|
+
if (!seen.has(key)) {
|
|
977
|
+
seen.add(key);
|
|
978
|
+
out.push(item);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return out;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Pluck a property from array of objects.
|
|
986
|
+
* @param {Array} arr
|
|
987
|
+
* @param {string} key
|
|
988
|
+
* @returns {Array}
|
|
989
|
+
*/
|
|
990
|
+
function pluck(arr, key) {
|
|
991
|
+
if (!Array.isArray(arr)) {
|
|
992
|
+
const args = Array.prototype.slice.call(arguments);
|
|
993
|
+
key = args.pop();
|
|
994
|
+
arr = args;
|
|
995
|
+
}
|
|
996
|
+
if (!Array.isArray(arr)) return [];
|
|
997
|
+
return arr.map(x => x && x[key]);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Sort by key or key function (ascending).
|
|
1002
|
+
* @param {Array} arr
|
|
1003
|
+
* @param {Function|string} keyFn
|
|
1004
|
+
* @returns {Array}
|
|
1005
|
+
*/
|
|
1006
|
+
function sortBy(arr, keyFn) {
|
|
1007
|
+
if (!Array.isArray(arr)) {
|
|
1008
|
+
const args = Array.prototype.slice.call(arguments);
|
|
1009
|
+
keyFn = args.pop();
|
|
1010
|
+
arr = args;
|
|
1011
|
+
}
|
|
1012
|
+
if (!Array.isArray(arr)) return [];
|
|
1013
|
+
const fn = typeof keyFn === "function" ? keyFn : (x) => x[keyFn];
|
|
1014
|
+
return arr.slice().sort((a, b) => {
|
|
1015
|
+
const ka = fn(a);
|
|
1016
|
+
const kb = fn(b);
|
|
1017
|
+
if (ka < kb) return -1;
|
|
1018
|
+
if (ka > kb) return 1;
|
|
1019
|
+
return 0;
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Round to a number of decimals.
|
|
1025
|
+
* @param {number} n
|
|
1026
|
+
* @param {number} decimals
|
|
1027
|
+
* @returns {number}
|
|
1028
|
+
*/
|
|
1029
|
+
function roundTo(n, decimals) {
|
|
1030
|
+
// If a place value like 100 or 1000 is provided, round to that place.
|
|
1031
|
+
if (typeof decimals === "number" && Number.isInteger(decimals) && Math.abs(decimals) >= 10) {
|
|
1032
|
+
const place = decimals;
|
|
1033
|
+
if (place === 0) return 0;
|
|
1034
|
+
return Math.round(Number(n) / place) * place;
|
|
1035
|
+
}
|
|
1036
|
+
const p = Math.pow(10, decimals || 0);
|
|
1037
|
+
return Math.round(Number(n) * p) / p;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Round to decimal places (e.g. hundredths = 2).
|
|
1042
|
+
* @param {number} n
|
|
1043
|
+
* @param {number|string} place
|
|
1044
|
+
* @returns {number}
|
|
1045
|
+
*/
|
|
1046
|
+
function roundDecimal(n, place) {
|
|
1047
|
+
const map = {
|
|
1048
|
+
tenths: 1,
|
|
1049
|
+
hundredths: 2,
|
|
1050
|
+
thousandths: 3,
|
|
1051
|
+
};
|
|
1052
|
+
const dec = typeof place === "string" ? (map[place] ?? 0) : place;
|
|
1053
|
+
const p = Math.pow(10, dec || 0);
|
|
1054
|
+
return Math.round(Number(n) * p) / p;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Floor a number.
|
|
1059
|
+
* @param {number} n
|
|
1060
|
+
* @returns {number}
|
|
1061
|
+
*/
|
|
1062
|
+
function roundDown(n) {
|
|
1063
|
+
return Math.floor(n);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Ceil a number.
|
|
1068
|
+
* @param {number} n
|
|
1069
|
+
* @returns {number}
|
|
1070
|
+
*/
|
|
1071
|
+
function roundUp(n) {
|
|
1072
|
+
return Math.ceil(n);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Linear interpolation.
|
|
1077
|
+
* @param {number} a
|
|
1078
|
+
* @param {number} b
|
|
1079
|
+
* @param {number} t
|
|
1080
|
+
* @returns {number}
|
|
1081
|
+
*/
|
|
1082
|
+
function lerp(a, b, t) {
|
|
1083
|
+
return a + (b - a) * t;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Convert part/total to percentage.
|
|
1088
|
+
* @param {number} part
|
|
1089
|
+
* @param {number} total
|
|
1090
|
+
* @returns {number}
|
|
1091
|
+
*/
|
|
1092
|
+
function percent(part, total) {
|
|
1093
|
+
if (total === 0) return 0;
|
|
1094
|
+
return (Number(part) / Number(total)) * 100;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Clamp to [0, 1].
|
|
1099
|
+
* @param {number} n
|
|
1100
|
+
* @returns {number}
|
|
1101
|
+
*/
|
|
1102
|
+
function clamp01(n) {
|
|
1103
|
+
return clamp(n, 0, 1);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Random integer between min and max inclusive.
|
|
1108
|
+
* @param {number} min
|
|
1109
|
+
* @param {number} max
|
|
1110
|
+
* @returns {number}
|
|
1111
|
+
*/
|
|
1112
|
+
function randomInt(min, max) {
|
|
1113
|
+
const a = Math.ceil(min);
|
|
1114
|
+
const b = Math.floor(max);
|
|
1115
|
+
return Math.floor(Math.random() * (b - a + 1)) + a;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Random float between min and max.
|
|
1120
|
+
* @param {number} min
|
|
1121
|
+
* @param {number} max
|
|
1122
|
+
* @returns {number}
|
|
1123
|
+
*/
|
|
1124
|
+
function randomFloat(min, max) {
|
|
1125
|
+
return Math.random() * (max - min) + min;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Shuffle array (Fisher-Yates).
|
|
1130
|
+
* @param {Array} arr
|
|
1131
|
+
* @returns {Array}
|
|
1132
|
+
*/
|
|
1133
|
+
function shuffle(arr) {
|
|
1134
|
+
if (!Array.isArray(arr)) arr = arrayFromArgs(arguments, []);
|
|
1135
|
+
if (!Array.isArray(arr)) return [];
|
|
1136
|
+
const out = arr.slice();
|
|
1137
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
1138
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1139
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
1140
|
+
}
|
|
1141
|
+
return out;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Sample one element from array.
|
|
1146
|
+
* @param {Array} arr
|
|
1147
|
+
* @returns {*}
|
|
1148
|
+
*/
|
|
1149
|
+
function sample(arr) {
|
|
1150
|
+
if (!Array.isArray(arr)) arr = arrayFromArgs(arguments, []);
|
|
1151
|
+
if (!Array.isArray(arr) || arr.length === 0) return undefined;
|
|
1152
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Deep clone (uses structuredClone when available).
|
|
1157
|
+
* @param {*} value
|
|
1158
|
+
* @returns {*}
|
|
1159
|
+
*/
|
|
1160
|
+
function deepClone(value) {
|
|
1161
|
+
if (typeof structuredClone === "function") return structuredClone(value);
|
|
1162
|
+
return JSON.parse(JSON.stringify(value));
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Safe JSON stringify with fallback.
|
|
1167
|
+
* @param {*} value
|
|
1168
|
+
* @param {*} fallback
|
|
1169
|
+
* @returns {string}
|
|
1170
|
+
*/
|
|
1171
|
+
function safeJsonStringify(value, fallback = "") {
|
|
1172
|
+
try {
|
|
1173
|
+
return JSON.stringify(value);
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
return String(fallback);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Move an item within an array.
|
|
1181
|
+
* @param {Array} arr
|
|
1182
|
+
* @param {number} fromIndex
|
|
1183
|
+
* @param {number} toIndex
|
|
1184
|
+
* @returns {Array}
|
|
1185
|
+
*/
|
|
1186
|
+
function move(arr, fromIndex, toIndex) {
|
|
1187
|
+
if (!Array.isArray(arr)) {
|
|
1188
|
+
const args = Array.prototype.slice.call(arguments);
|
|
1189
|
+
toIndex = args.pop();
|
|
1190
|
+
fromIndex = args.pop();
|
|
1191
|
+
arr = args;
|
|
1192
|
+
}
|
|
1193
|
+
if (!Array.isArray(arr)) return [];
|
|
1194
|
+
const out = arr.slice();
|
|
1195
|
+
const len = out.length;
|
|
1196
|
+
const from = fromIndex < 0 ? len + fromIndex : fromIndex;
|
|
1197
|
+
const to = toIndex < 0 ? len + toIndex : toIndex;
|
|
1198
|
+
if (from < 0 || from >= len || to < 0 || to >= len) return out;
|
|
1199
|
+
const [item] = out.splice(from, 1);
|
|
1200
|
+
out.splice(to, 0, item);
|
|
1201
|
+
return out;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Group array items by key function.
|
|
1206
|
+
* @param {Array} arr
|
|
1207
|
+
* @param {Function} keyFn
|
|
1208
|
+
* @returns {Object}
|
|
1209
|
+
*/
|
|
1210
|
+
function arrayGroupBy(arr, keyFn) {
|
|
1211
|
+
return groupBy(arr, keyFn);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Map object keys to a new object.
|
|
1216
|
+
* @param {Object} obj
|
|
1217
|
+
* @param {Function} keyFn
|
|
1218
|
+
* @returns {Object}
|
|
1219
|
+
*/
|
|
1220
|
+
function mapKeys(obj, keyFn) {
|
|
1221
|
+
if (!obj || typeof obj !== "object") return {};
|
|
1222
|
+
const out = {};
|
|
1223
|
+
Object.keys(obj).forEach(k => {
|
|
1224
|
+
const nk = keyFn(k, obj[k], obj);
|
|
1225
|
+
out[nk] = obj[k];
|
|
1226
|
+
});
|
|
1227
|
+
return out;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Invert keys and values.
|
|
1232
|
+
* @param {Object} obj
|
|
1233
|
+
* @returns {Object}
|
|
1234
|
+
*/
|
|
1235
|
+
function invert(obj) {
|
|
1236
|
+
if (!obj || typeof obj !== "object") return {};
|
|
1237
|
+
const out = {};
|
|
1238
|
+
Object.keys(obj).forEach(k => {
|
|
1239
|
+
out[String(obj[k])] = k;
|
|
1240
|
+
});
|
|
1241
|
+
return out;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Remove null/undefined/empty properties from object.
|
|
1246
|
+
* @param {Object} obj
|
|
1247
|
+
* @returns {Object}
|
|
1248
|
+
*/
|
|
1249
|
+
function objectCompact(obj) {
|
|
1250
|
+
if (!obj || typeof obj !== "object") return {};
|
|
1251
|
+
const out = {};
|
|
1252
|
+
Object.keys(obj).forEach(k => {
|
|
1253
|
+
if (!isEmpty(obj[k])) out[k] = obj[k];
|
|
1254
|
+
});
|
|
1255
|
+
return out;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Convert a string to Title Case.
|
|
1260
|
+
* @param {*} s
|
|
1261
|
+
* @returns {string}
|
|
1262
|
+
*/
|
|
1263
|
+
function toTitleCase(s) {
|
|
1264
|
+
return String(s)
|
|
1265
|
+
.toLowerCase()
|
|
1266
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Truncate string to length with ellipsis.
|
|
1271
|
+
* @param {*} s
|
|
1272
|
+
* @param {number} length
|
|
1273
|
+
* @returns {string}
|
|
1274
|
+
*/
|
|
1275
|
+
function truncate(s, length) {
|
|
1276
|
+
const str = String(s);
|
|
1277
|
+
if (length <= 3) return str.slice(0, length);
|
|
1278
|
+
if (str.length <= length) return str;
|
|
1279
|
+
return str.slice(0, length - 3) + "...";
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* Replace all occurrences of substring.
|
|
1284
|
+
* @param {*} s
|
|
1285
|
+
* @param {string} substring
|
|
1286
|
+
* @param {string} newSub
|
|
1287
|
+
* @returns {string}
|
|
1288
|
+
*/
|
|
1289
|
+
function replaceAll(s, substring, newSub) {
|
|
1290
|
+
return String(s).split(substring).join(newSub);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* Count words in a string.
|
|
1295
|
+
* @param {*} s
|
|
1296
|
+
* @returns {number}
|
|
1297
|
+
*/
|
|
1298
|
+
function countWords(s) {
|
|
1299
|
+
const words = String(s).trim().match(/\b[\w']+\b/g);
|
|
1300
|
+
return words ? words.length : 0;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Install convenience methods on Array/Object/String prototypes.
|
|
1305
|
+
* @returns {void}
|
|
1306
|
+
*/
|
|
1307
|
+
function install() {
|
|
1308
|
+
const define = (proto, name, fn) => {
|
|
1309
|
+
if (!Object.prototype.hasOwnProperty.call(proto, name)) {
|
|
1310
|
+
Object.defineProperty(proto, name, {
|
|
1311
|
+
value: fn,
|
|
1312
|
+
enumerable: false,
|
|
1313
|
+
configurable: true,
|
|
1314
|
+
writable: true,
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
define(Array.prototype, "shuffle", function () { return shuffle(this); });
|
|
1320
|
+
define(Array.prototype, "unique", function () { return uniq(this); });
|
|
1321
|
+
define(Array.prototype, "chunk", function (size) { return chunk(this, size); });
|
|
1322
|
+
define(Array.prototype, "move", function (from, to) { return move(this, from, to); });
|
|
1323
|
+
define(Array.prototype, "groupBy", function (fn) { return arrayGroupBy(this, fn); });
|
|
1324
|
+
define(Array.prototype, "compact", function (fn) { return compact(this, fn); });
|
|
1325
|
+
define(Array.prototype, "flatten", function () { return flatten(this); });
|
|
1326
|
+
define(Array.prototype, "first", function () { return first(this); });
|
|
1327
|
+
define(Array.prototype, "last", function () { return last(this); });
|
|
1328
|
+
define(Array.prototype, "take", function (n) { return take(this, n); });
|
|
1329
|
+
define(Array.prototype, "takeRight", function (n) { return takeRight(this, n); });
|
|
1330
|
+
define(Array.prototype, "drop", function (n) { return drop(this, n); });
|
|
1331
|
+
define(Array.prototype, "dropRight", function (n) { return dropRight(this, n); });
|
|
1332
|
+
|
|
1333
|
+
define(Object.prototype, "mapKeys", function (fn) { return mapKeys(this, fn); });
|
|
1334
|
+
define(Object.prototype, "pick", function (keys) { return pick(this, keys); });
|
|
1335
|
+
define(Object.prototype, "invert", function () { return invert(this); });
|
|
1336
|
+
define(Object.prototype, "compact", function () { return objectCompact(this); });
|
|
1337
|
+
define(Object.prototype, "omit", function (keys) { return omit(this, keys); });
|
|
1338
|
+
define(Object.prototype, "has", function (key) { return has(this, key); });
|
|
1339
|
+
define(Object.prototype, "merge", function () { return merge.apply(null, [this].concat(Array.prototype.slice.call(arguments))); });
|
|
1340
|
+
define(Object.prototype, "deepMerge", function () { return deepMerge.apply(null, [this].concat(Array.prototype.slice.call(arguments))); });
|
|
1341
|
+
define(Object.prototype, "size", function () { return size(this); });
|
|
1342
|
+
|
|
1343
|
+
define(String.prototype, "toTitleCase", function () { return toTitleCase(this); });
|
|
1344
|
+
define(String.prototype, "truncate", function (len) { return truncate(this, len); });
|
|
1345
|
+
define(String.prototype, "replaceAllEasy", function (sub, rep) { return replaceAll(this, sub, rep); });
|
|
1346
|
+
define(String.prototype, "countWords", function () { return countWords(this); });
|
|
1347
|
+
define(String.prototype, "slugify", function () { return slugify(this); });
|
|
1348
|
+
define(String.prototype, "padLeft", function (len, ch) { return padLeft(this, len, ch); });
|
|
1349
|
+
define(String.prototype, "padRight", function (len, ch) { return padRight(this, len, ch); });
|
|
1350
|
+
define(String.prototype, "repeatEasy", function (times) { return repeat(this, times); });
|
|
1351
|
+
define(String.prototype, "stripNonAlpha", function () { return stripNonAlpha(this); });
|
|
1352
|
+
define(String.prototype, "isBlank", function () { return isBlank(this); });
|
|
1353
|
+
define(String.prototype, "normalizeString", function () { return normalizeString(this); });
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
module.exports = {
|
|
1357
|
+
identity,
|
|
1358
|
+
noop,
|
|
1359
|
+
isNil,
|
|
1360
|
+
isEmpty,
|
|
1361
|
+
defaultTo,
|
|
1362
|
+
toLower,
|
|
1363
|
+
toUpper,
|
|
1364
|
+
capitalize,
|
|
1365
|
+
trim,
|
|
1366
|
+
normalizeString,
|
|
1367
|
+
slugify,
|
|
1368
|
+
padLeft,
|
|
1369
|
+
padRight,
|
|
1370
|
+
repeat,
|
|
1371
|
+
stripNonAlpha,
|
|
1372
|
+
isBlank,
|
|
1373
|
+
asArray,
|
|
1374
|
+
anyOf,
|
|
1375
|
+
eqAny,
|
|
1376
|
+
inArray,
|
|
1377
|
+
clamp,
|
|
1378
|
+
between,
|
|
1379
|
+
toInt,
|
|
1380
|
+
toFloat,
|
|
1381
|
+
toNumber,
|
|
1382
|
+
isNumber,
|
|
1383
|
+
isInt,
|
|
1384
|
+
compact,
|
|
1385
|
+
uniq,
|
|
1386
|
+
flatten,
|
|
1387
|
+
chunk,
|
|
1388
|
+
range,
|
|
1389
|
+
min,
|
|
1390
|
+
max,
|
|
1391
|
+
sum,
|
|
1392
|
+
avg,
|
|
1393
|
+
median,
|
|
1394
|
+
mode,
|
|
1395
|
+
percentile,
|
|
1396
|
+
groupBy,
|
|
1397
|
+
first,
|
|
1398
|
+
last,
|
|
1399
|
+
take,
|
|
1400
|
+
takeRight,
|
|
1401
|
+
drop,
|
|
1402
|
+
dropRight,
|
|
1403
|
+
zip,
|
|
1404
|
+
unzip,
|
|
1405
|
+
intersection,
|
|
1406
|
+
difference,
|
|
1407
|
+
union,
|
|
1408
|
+
pick,
|
|
1409
|
+
omit,
|
|
1410
|
+
has,
|
|
1411
|
+
keys,
|
|
1412
|
+
values,
|
|
1413
|
+
entries,
|
|
1414
|
+
fromEntries,
|
|
1415
|
+
merge,
|
|
1416
|
+
deepMerge,
|
|
1417
|
+
isObject,
|
|
1418
|
+
size,
|
|
1419
|
+
safeGet,
|
|
1420
|
+
safeSet,
|
|
1421
|
+
tryCatch,
|
|
1422
|
+
tap,
|
|
1423
|
+
sleep,
|
|
1424
|
+
containsAny,
|
|
1425
|
+
startsWithAny,
|
|
1426
|
+
endsWithAny,
|
|
1427
|
+
safeJsonParse,
|
|
1428
|
+
once,
|
|
1429
|
+
debounce,
|
|
1430
|
+
throttle,
|
|
1431
|
+
uniqueBy,
|
|
1432
|
+
pluck,
|
|
1433
|
+
sortBy,
|
|
1434
|
+
roundTo,
|
|
1435
|
+
roundDecimal,
|
|
1436
|
+
roundDown,
|
|
1437
|
+
roundUp,
|
|
1438
|
+
lerp,
|
|
1439
|
+
percent,
|
|
1440
|
+
clamp01,
|
|
1441
|
+
randomInt,
|
|
1442
|
+
randomFloat,
|
|
1443
|
+
shuffle,
|
|
1444
|
+
sample,
|
|
1445
|
+
deepClone,
|
|
1446
|
+
safeJsonStringify,
|
|
1447
|
+
move,
|
|
1448
|
+
arrayGroupBy,
|
|
1449
|
+
mapKeys,
|
|
1450
|
+
invert,
|
|
1451
|
+
objectCompact,
|
|
1452
|
+
toTitleCase,
|
|
1453
|
+
truncate,
|
|
1454
|
+
replaceAll,
|
|
1455
|
+
countWords,
|
|
1456
|
+
install,
|
|
1457
|
+
};
|