@lowentry/utils 1.15.3 → 1.16.1

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.
@@ -1,45 +1,662 @@
1
- import {describe, test, expect} from '@jest/globals';
1
+ import {describe, test, expect, jest} from '@jest/globals';
2
2
  import {LeUtils} from '../src/index.js';
3
3
 
4
+ const wait = ms => LeUtils.promiseTimeout(ms ?? 100);
5
+
4
6
 
5
7
  describe('LeUtils.each()', () =>
6
8
  {
7
- test('iterates arrays', () =>
9
+ test('array order', () =>
8
10
  {
9
- const arr = [1, 2, 3];
10
- const seen = [];
11
- LeUtils.each(arr, value =>
11
+ const out = [];
12
+ LeUtils.each([1, 2, 3], v =>
12
13
  {
13
- seen.push(value);
14
+ out.push(v);
14
15
  });
15
- expect(seen).toEqual([1, 2, 3]);
16
+ expect(out).toEqual([1, 2, 3]);
16
17
  });
17
-
18
18
  test('breaks on false', () =>
19
19
  {
20
- const arr = [1, 2, 3];
20
+ const out = [];
21
+ LeUtils.each([1, 2, 3], v =>
22
+ {
23
+ out.push(v);
24
+ if(v === 2)
25
+ {
26
+ return false;
27
+ }
28
+ });
29
+ expect(out).toEqual([1, 2]);
30
+ });
31
+ test('Map iteration', () =>
32
+ {
33
+ const m = new Map([['a', 1], ['b', 2]]);
34
+ const seen = [];
35
+ LeUtils.each(m, (v, k) =>
36
+ {
37
+ seen.push([k, v]);
38
+ });
39
+ expect(seen).toEqual([['a', 1], ['b', 2]]);
40
+ });
41
+ test('Set iteration', () =>
42
+ {
43
+ const s = new Set([10, 20]);
44
+ const seen = [];
45
+ LeUtils.each(s, (v, k) =>
46
+ {
47
+ seen.push([k, v]);
48
+ });
49
+ expect(seen).toEqual([[10, 10], [20, 20]]);
50
+ });
51
+ test('string iteration', () =>
52
+ {
21
53
  const seen = [];
22
- LeUtils.each(arr, value =>
54
+ LeUtils.each('ab', (c, i) =>
23
55
  {
24
- seen.push(value);
25
- if(value === 2)
56
+ seen.push([i, c]);
57
+ });
58
+ expect(seen).toEqual([[0, 'a'], [1, 'b']]);
59
+ });
60
+ test('object skips prototype props', () =>
61
+ {
62
+ const proto = {z:9};
63
+ const o = Object.create(proto);
64
+ o.a = 1;
65
+ o.b = 2;
66
+ const seen = [];
67
+ LeUtils.each(o, v =>
68
+ {
69
+ seen.push(v);
70
+ });
71
+ expect(seen).toEqual([1, 2]);
72
+ });
73
+ });
74
+
75
+
76
+ describe('LeUtils.supportsEach()', () =>
77
+ {
78
+ test('truthy for iterable types', () =>
79
+ {
80
+ expect(LeUtils.supportsEach([1])).toBe(true);
81
+ expect(LeUtils.supportsEach(new Map())).toBe(true);
82
+ expect(LeUtils.supportsEach(new Set())).toBe(true);
83
+ expect(LeUtils.supportsEach('x')).toBe(true);
84
+ });
85
+ test('false for primitives', () =>
86
+ {
87
+ expect(LeUtils.supportsEach(5)).toBe(false);
88
+ expect(LeUtils.supportsEach(null)).toBe(false);
89
+ expect(LeUtils.supportsEach(undefined)).toBe(false);
90
+ });
91
+ });
92
+
93
+
94
+ describe('LeUtils.getValueAtIndex()', () =>
95
+ {
96
+ test('array index', () =>
97
+ {
98
+ expect(LeUtils.getValueAtIndex([4, 5, 6], 1)).toBe(5);
99
+ });
100
+ test('Map key', () =>
101
+ {
102
+ const m = new Map([['k', 99]]);
103
+ expect(LeUtils.getValueAtIndex(m, 'k')).toBe(99);
104
+ });
105
+ test('Set returns key itself', () =>
106
+ {
107
+ const s = new Set([7]);
108
+ expect(LeUtils.getValueAtIndex(s, 7)).toBe(7);
109
+ });
110
+ });
111
+
112
+
113
+ describe('LeUtils.filter()', () =>
114
+ {
115
+ test('array truthy filter default', () =>
116
+ {
117
+ const out = LeUtils.filter([0, 1, '', 'x', false, true]);
118
+ expect(out).toEqual([1, 'x', true]);
119
+ });
120
+ test('object value > 1', () =>
121
+ {
122
+ const res = LeUtils.filter({a:1, b:2, c:3}, v => v > 1);
123
+ expect(res).toEqual({b:2, c:3});
124
+ });
125
+ test('Map retain even', () =>
126
+ {
127
+ const m = new Map([['a', 1], ['b', 2]]);
128
+ const out = LeUtils.filter(m, v => v % 2 === 0);
129
+ expect(out instanceof Map).toBe(true);
130
+ expect(out.get('b')).toBe(2);
131
+ });
132
+ });
133
+
134
+
135
+ describe('LeUtils.map utilities', () =>
136
+ {
137
+ test('map array double', () =>
138
+ {
139
+ expect(LeUtils.map([1, 2], n => n * 2)).toEqual([2, 4]);
140
+ });
141
+ test('map object values++', () =>
142
+ {
143
+ expect(LeUtils.map({x:1}, v => v + 1)).toEqual({x:2});
144
+ });
145
+ test('map typed array', () =>
146
+ {
147
+ const ta = new Uint8Array([1, 2]);
148
+ expect(LeUtils.map(ta, n => n + 3)).toEqual([4, 5]);
149
+ });
150
+ test('mapToArray', () =>
151
+ {
152
+ const m = new Map([['k', 2]]);
153
+ expect(LeUtils.mapToArray(m, v => v * 3)).toEqual([6]);
154
+ });
155
+ test('mapToArraySorted', () =>
156
+ {
157
+ const o = {a:3, b:1, c:2};
158
+ const arr = LeUtils.mapToArraySorted(o, (x, y) => x - y, v => v);
159
+ expect(arr).toEqual([1, 2, 3]);
160
+ });
161
+ });
162
+
163
+
164
+ describe('LeUtils.sortKeys()', () =>
165
+ {
166
+ test('object keys by descending value', () =>
167
+ {
168
+ const obj = {a:1, b:3, c:2};
169
+ const keys = LeUtils.sortKeys(obj, (x, y) => y - x);
170
+ expect(keys).toEqual(['b', 'c', 'a']);
171
+ });
172
+ });
173
+
174
+
175
+ describe('LeUtils flatten helpers', () =>
176
+ {
177
+ test('flattenArray', () =>
178
+ {
179
+ expect(LeUtils.flattenArray([1, [2, [3]]])).toEqual([1, 2, 3]);
180
+ });
181
+ test('flattenToArray mixed', () =>
182
+ {
183
+ const mixed = [1, new Set([2, 3]), {x:4}, new Map([['a', 5], ['b', 6]])];
184
+ expect(LeUtils.flattenToArray(mixed)).toEqual([1, 2, 3, 4, 5, 6]);
185
+ });
186
+ });
187
+
188
+
189
+ describe('LeUtils.getEmptySimplifiedCollection()', () =>
190
+ {
191
+ test('array source returns array add()', () =>
192
+ {
193
+ const [ok, collection, add] = LeUtils.getEmptySimplifiedCollection([5]);
194
+ expect(ok).toBe(true);
195
+ add(1, 0);
196
+ add(2, 1);
197
+ expect(collection).toEqual([1, 2]);
198
+ });
199
+ test('map source returns Map add()', () =>
200
+ {
201
+ const [ok, collection, add] = LeUtils.getEmptySimplifiedCollection(new Map());
202
+ expect(ok).toBe(true);
203
+ add('v', 'k');
204
+ expect(collection instanceof Map).toBe(true);
205
+ expect(collection.get('k')).toBe('v');
206
+ });
207
+ });
208
+
209
+
210
+ describe('LeUtils.eachAsync()', () =>
211
+ {
212
+ test('serial preserves order', async () =>
213
+ {
214
+ const seen = [];
215
+ await LeUtils.eachAsync([1, 2, 3], async v =>
216
+ {
217
+ await wait();
218
+ seen.push(v);
219
+ }, 1);
220
+ expect(seen).toEqual([1, 2, 3]);
221
+ });
222
+ test('parallel limit honoured', async () =>
223
+ {
224
+ const active = [];
225
+ const max = [];
226
+ await LeUtils.eachAsync([1, 2, 3, 4, 5], async v =>
227
+ {
228
+ active.push(v);
229
+ max.push(active.length);
230
+ await wait();
231
+ active.splice(active.indexOf(v), 1);
232
+ }, 2);
233
+ expect(Math.max(...max)).toBeLessThanOrEqual(2);
234
+ });
235
+ test('early break stops queuing', async () =>
236
+ {
237
+ const processed = [];
238
+ await LeUtils.eachAsync([1, 2, 3, 4], async v =>
239
+ {
240
+ processed.push(v);
241
+ if(v === 2)
26
242
  {
27
243
  return false;
28
244
  }
245
+ await wait();
246
+ }, 2);
247
+ expect(processed).toContain(1);
248
+ expect(processed).toContain(2);
249
+ expect(processed.length).toBeLessThan(4);
250
+ });
251
+ test('works on Map', async () =>
252
+ {
253
+ const m = new Map([['a', 1], ['b', 2]]);
254
+ const out = [];
255
+ await LeUtils.eachAsync(m, async (v, k) =>
256
+ {
257
+ out.push([k, v]);
258
+ });
259
+ expect(out).toEqual([['a', 1], ['b', 2]]);
260
+ });
261
+ test('works on Set', async () =>
262
+ {
263
+ const s = new Set([3, 4]);
264
+ const out = [];
265
+ await LeUtils.eachAsync(s, async (v, k) =>
266
+ {
267
+ out.push([k, v]);
268
+ });
269
+ expect(out).toEqual([[3, 3], [4, 4]]);
270
+ });
271
+ });
272
+
273
+
274
+ describe('LeUtils.getEmptySimplifiedCollection()', () =>
275
+ {
276
+ test('string source returns array add()', () =>
277
+ {
278
+ const [ok, collection, add] = LeUtils.getEmptySimplifiedCollection('abc');
279
+ expect(ok).toBe(true);
280
+ ['x', 'y'].forEach(add);
281
+ expect(collection).toEqual(['x', 'y']);
282
+ });
283
+ test('custom iterable source object returns object', () =>
284
+ {
285
+ const iterable = {
286
+ * [Symbol.iterator]()
287
+ {
288
+ yield 1;
289
+ },
290
+ };
291
+ const [ok, collection, add] = LeUtils.getEmptySimplifiedCollection(iterable);
292
+ expect(ok).toBe(true);
293
+ expect(Array.isArray(collection)).toBe(false);
294
+ add(2, '0');
295
+ add(3, '1');
296
+ add(4, '2');
297
+ expect(collection).toEqual({0:2, 1:3, 2:4});
298
+ });
299
+ test('custom iterable source returns array', () =>
300
+ {
301
+ class CustomIterable
302
+ {
303
+ * [Symbol.iterator]()
304
+ {
305
+ yield 1;
306
+ }
307
+ }
308
+
309
+ const iterable = new CustomIterable();
310
+ const [ok, collection, add] = LeUtils.getEmptySimplifiedCollection(iterable);
311
+ expect(ok).toBe(true);
312
+ expect(Array.isArray(collection)).toBe(true);
313
+ add(2, 0);
314
+ add(3, 1);
315
+ add(4, 2);
316
+ expect(collection).toEqual([2, 3, 4]);
317
+ });
318
+ });
319
+
320
+
321
+ describe('LeUtils.eachIterator()', () =>
322
+ {
323
+ test('Map yields [value,key] in order', () =>
324
+ {
325
+ const m = new Map([['a', 1], ['b', 2]]);
326
+ const out = [...LeUtils.eachIterator(m)];
327
+ expect(out).toEqual([[1, 'a'], [2, 'b']]);
328
+ });
329
+ test('string yields chars', () =>
330
+ {
331
+ const out = [...LeUtils.eachIterator('xyz')];
332
+ expect(out).toEqual([['x', 0], ['y', 1], ['z', 2]]);
333
+ });
334
+ });
335
+
336
+
337
+ describe('LeUtils.filter() edge', () =>
338
+ {
339
+ test('Map truthy default retains only truthy', () =>
340
+ {
341
+ const m = new Map([['a', 0], ['b', 2]]);
342
+ const res = LeUtils.filter(m);
343
+ expect(res instanceof Map).toBe(true);
344
+ expect([...res.entries()]).toEqual([['b', 2]]);
345
+ });
346
+ });
347
+
348
+
349
+ describe('LeUtils.map() identity on Map', () =>
350
+ {
351
+ test('returns Map clone when callback omitted', () =>
352
+ {
353
+ const m = new Map([['k', 5]]);
354
+ const res = LeUtils.map(m);
355
+ expect(res instanceof Map).toBe(true);
356
+ expect(res.get('k')).toBe(5);
357
+ expect(res).not.toBe(m);
358
+ });
359
+ });
360
+
361
+
362
+ describe('LeUtils.mapToArraySorted() on Map', () =>
363
+ {
364
+ test('sorts by squared value', () =>
365
+ {
366
+ const m = new Map([['x', 2], ['y', 3]]);
367
+ const arr = LeUtils.mapToArraySorted(
368
+ m,
369
+ (a, b) => a - b,
370
+ v => v * v,
371
+ );
372
+ expect(arr).toEqual([4, 9]);
373
+ });
374
+ });
375
+
376
+
377
+ describe('LeUtils.sortKeys() on object with string length comparator', () =>
378
+ {
379
+ test('sorts keys by length of value string', () =>
380
+ {
381
+ const obj = {a:'tool', b:'hi', c:'alpha'};
382
+ const keys = LeUtils.sortKeys(obj, (x, y) => x.length - y.length);
383
+ expect(keys).toEqual(['b', 'a', 'c']);
384
+ });
385
+ });
386
+
387
+
388
+ describe('LeUtils.flattenArray and flattenToArray deep mix', () =>
389
+ {
390
+ test('flattenArray non-array passthrough', () =>
391
+ {
392
+ expect(LeUtils.flattenArray(7)).toEqual([7]);
393
+ });
394
+ test('flattenToArray deep nested mix', () =>
395
+ {
396
+ const mixed = [1, [2, new Set([3]), {a:4}], new Map([['z', 5], ['', 6]])];
397
+ const flat = LeUtils.flattenToArray(mixed);
398
+ expect(flat).toEqual([1, 2, 3, 4, 5, 6]);
399
+ });
400
+ });
401
+
402
+
403
+ describe('LeUtils.supportsEach() custom forEach object and function', () =>
404
+ {
405
+ test('object with forEach()', () =>
406
+ {
407
+ const obj = {
408
+ forEach:() =>
409
+ {
410
+ },
411
+ };
412
+ expect(LeUtils.supportsEach(obj)).toBe(true);
413
+ });
414
+ test('plain function returns true', () =>
415
+ {
416
+ const fn = () =>
417
+ {
418
+ };
419
+ expect(LeUtils.supportsEach(fn)).toBe(true);
420
+ });
421
+ });
422
+
423
+
424
+ describe('LeUtils.eachAsync() heavy parallel', () =>
425
+ {
426
+ jest.setTimeout(10000);
427
+ test('processes 100 items with concurrency 20', async () =>
428
+ {
429
+ const size = 100;
430
+ const data = [...Array(size).keys()];
431
+ const active = [];
432
+ const max = [];
433
+ await LeUtils.eachAsync(
434
+ data,
435
+ async v =>
436
+ {
437
+ active.push(v);
438
+ max.push(active.length);
439
+ await wait();
440
+ active.splice(active.indexOf(v), 1);
441
+ },
442
+ 20,
443
+ );
444
+ expect(max.length).toBe(size);
445
+ expect(Math.max(...max)).toBeLessThanOrEqual(20);
446
+ });
447
+ test('serial path matches sequential order with strings', async () =>
448
+ {
449
+ const seen = [];
450
+ await LeUtils.eachAsync('abcd', async (c, i) =>
451
+ {
452
+ await wait();
453
+ seen.push([i, c]);
454
+ });
455
+ expect(seen).toEqual([[0, 'a'], [1, 'b'], [2, 'c'], [3, 'd']]);
456
+ });
457
+ });
458
+
459
+
460
+ describe('LeUtils.getValueAtIndex() custom iterable', () =>
461
+ {
462
+ test('returns value from generator sequence', () =>
463
+ {
464
+ class CustomIterable
465
+ {
466
+ * [Symbol.iterator]()
467
+ {
468
+ yield 5;
469
+ yield 6;
470
+ }
471
+ }
472
+
473
+ const gen = new CustomIterable();
474
+ expect(LeUtils.getValueAtIndex(gen, 0)).toBe(5);
475
+ expect(LeUtils.getValueAtIndex(gen, 1)).toBe(6);
476
+ });
477
+ });
478
+
479
+
480
+ describe('LeUtils.each() extra', () =>
481
+ {
482
+ test('typed array', () =>
483
+ {
484
+ const ta = new Uint16Array([9, 8]);
485
+ const out = [];
486
+ LeUtils.each(ta, (v, i) =>
487
+ {
488
+ out.push([i, v]);
489
+ });
490
+ expect(out).toEqual([[0, 9], [1, 8]]);
491
+ });
492
+ test('custom forEach object', () =>
493
+ {
494
+ const obj = {
495
+ forEach(cb)
496
+ {
497
+ [1, 2].forEach(cb);
498
+ },
499
+ };
500
+ const seen = [];
501
+ LeUtils.each(obj, (v, k) =>
502
+ {
503
+ seen.push(k);
504
+ });
505
+ expect(seen).toEqual(['forEach']);
506
+ });
507
+ test('custom forEach class', () =>
508
+ {
509
+ class ForEachObject
510
+ {
511
+ forEach(cb)
512
+ {
513
+ [1, 2].forEach(cb);
514
+ }
515
+ }
516
+
517
+ const obj = new ForEachObject();
518
+ const seen = [];
519
+ LeUtils.each(obj, v =>
520
+ {
521
+ seen.push(v);
29
522
  });
30
523
  expect(seen).toEqual([1, 2]);
31
524
  });
32
-
33
- test('iterates maps', () =>
525
+ test('custom forEach class returns false', () =>
34
526
  {
35
- const m = new Map();
36
- m.set('a', 1);
37
- m.set('b', 2);
527
+ class ForEachObject
528
+ {
529
+ forEach(cb)
530
+ {
531
+ [1, 2].forEach(cb);
532
+ }
533
+ }
534
+
535
+ const obj = new ForEachObject();
38
536
  const seen = [];
39
- LeUtils.each(m, (value, index) =>
537
+ LeUtils.each(obj, v =>
40
538
  {
41
- seen.push([index, value]);
539
+ seen.push(v);
540
+ if(v === 1)
541
+ {
542
+ return false;
543
+ }
42
544
  });
43
- expect(seen).toEqual([['a', 1], ['b', 2]]);
545
+ expect(seen).toEqual([1]);
546
+ });
547
+ });
548
+
549
+
550
+ describe('LeUtils.supportsEach() extra', () =>
551
+ {
552
+ test('typed array true', () =>
553
+ {
554
+ expect(LeUtils.supportsEach(new Int8Array(2))).toBe(true);
555
+ });
556
+ });
557
+
558
+
559
+ describe('LeUtils.getValueAtIndex() extra', () =>
560
+ {
561
+ test('string charAt', () =>
562
+ {
563
+ expect(LeUtils.getValueAtIndex('xyz', 2)).toBe('z');
564
+ });
565
+ });
566
+
567
+
568
+ describe('LeUtils.filter() extra', () =>
569
+ {
570
+ test('Set keep odd', () =>
571
+ {
572
+ const s = new Set([1, 2, 3]);
573
+ const res = LeUtils.filter(s, v => v % 2 === 1);
574
+ expect(res).toEqual([1, 3]);
575
+ });
576
+ });
577
+
578
+
579
+ describe('LeUtils.mapToArraySorted() extra', () =>
580
+ {
581
+ test('numeric array sort descending', () =>
582
+ {
583
+ const arr = [3, 1, 2];
584
+ const res = LeUtils.mapToArraySorted(arr, (a, b) => b - a, v => v);
585
+ expect(res).toEqual([3, 2, 1]);
586
+ });
587
+ });
588
+
589
+
590
+ describe('LeUtils.sortKeys() extra', () =>
591
+ {
592
+ test('Map keys by numeric value asc', () =>
593
+ {
594
+ const m = new Map([['x', 5], ['y', 2]]);
595
+ const keys = LeUtils.sortKeys(m, (a, b) => a - b);
596
+ expect(keys).toEqual(['y', 'x']);
597
+ });
598
+ });
599
+
600
+
601
+ describe('LeUtils.flattenArray/flattenToArray extra', () =>
602
+ {
603
+ test('flattenArray one-level', () =>
604
+ {
605
+ expect(LeUtils.flattenArray([1, [2]])).toEqual([1, 2]);
606
+ });
607
+ test('flattenToArray skips empty arrays', () =>
608
+ {
609
+ const res = LeUtils.flattenToArray([[], [1], new Set()]);
610
+ expect(res).toEqual([1]);
611
+ });
612
+ });
613
+
614
+
615
+ describe('LeUtils.getEmptySimplifiedCollection() extra', () =>
616
+ {
617
+ test('function source returns object', () =>
618
+ {
619
+ const fn = () =>
620
+ {
621
+ };
622
+ const [ok, collection, add] = LeUtils.getEmptySimplifiedCollection(fn);
623
+ expect(ok).toBe(true);
624
+ add('v', 'k');
625
+ expect(collection).toEqual({k:'v'});
626
+ });
627
+ });
628
+
629
+
630
+ describe('LeUtils.eachAsync() stress 1000 items', () =>
631
+ {
632
+ jest.setTimeout(20000);
633
+ test('parallel 200 completes', async () =>
634
+ {
635
+ const N = 1000;
636
+ const src = [...Array(N).keys()];
637
+ let count = 0;
638
+ await LeUtils.eachAsync(src, async () =>
639
+ {
640
+ count++;
641
+ await wait();
642
+ }, 200);
643
+ expect(count).toBe(N);
644
+ });
645
+ test('early false stops further enqueues', async () =>
646
+ {
647
+ const list = [...Array(50).keys()];
648
+ const seen = [];
649
+ await LeUtils.eachAsync(list, async v =>
650
+ {
651
+ seen.push(v);
652
+ if(v === 10)
653
+ {
654
+ return false;
655
+ }
656
+ await wait();
657
+ }, 5);
658
+ expect(seen.includes(0)).toBe(true);
659
+ expect(seen.includes(10)).toBe(true);
660
+ expect(seen.length).toBeLessThan(50);
44
661
  });
45
662
  });
File without changes