@tim-code/my-util 0.5.7 → 0.5.9
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/package.json +2 -2
- package/src/array.js +40 -17
- package/src/array.test.js +93 -11
- package/src/find.test.js +0 -1
- package/src/math.test.js +0 -2
- package/src/object.test.js +0 -1
- package/src/promise.test.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tim-code/my-util",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Tim Sprowl",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@jest/globals": "^29.7.0",
|
|
32
|
-
"@tim-code/eslint-config": "^1.
|
|
32
|
+
"@tim-code/eslint-config": "^1.4.0",
|
|
33
33
|
"jest": "^29.7.0"
|
|
34
34
|
},
|
|
35
35
|
"jest": {
|
package/src/array.js
CHANGED
|
@@ -186,17 +186,16 @@ export function multilevel(...comparators) {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
function siftDown(heap, i, compare) {
|
|
190
|
-
const n = heap.length
|
|
189
|
+
function siftDown(heap, i, compare, length) {
|
|
191
190
|
// eslint-disable-next-line no-constant-condition
|
|
192
191
|
while (true) {
|
|
193
192
|
const left = 2 * i + 1
|
|
194
193
|
const right = left + 1
|
|
195
194
|
let largest = i
|
|
196
|
-
if (left <
|
|
195
|
+
if (left < length && compare(heap[left], heap[largest]) > 0) {
|
|
197
196
|
largest = left
|
|
198
197
|
}
|
|
199
|
-
if (right <
|
|
198
|
+
if (right < length && compare(heap[right], heap[largest]) > 0) {
|
|
200
199
|
largest = right
|
|
201
200
|
}
|
|
202
201
|
if (largest === i) {
|
|
@@ -209,11 +208,11 @@ function siftDown(heap, i, compare) {
|
|
|
209
208
|
}
|
|
210
209
|
}
|
|
211
210
|
|
|
212
|
-
function maxHeapify(heap, compare) {
|
|
211
|
+
function maxHeapify(heap, compare, length) {
|
|
213
212
|
// (heap.length >>> 1) is equivalent to Math.floor(heap.length / 2)
|
|
214
213
|
// eslint-disable-next-line no-bitwise
|
|
215
|
-
for (let i = (
|
|
216
|
-
siftDown(heap, i, compare)
|
|
214
|
+
for (let i = (length >>> 1) - 1; i >= 0; i--) {
|
|
215
|
+
siftDown(heap, i, compare, length)
|
|
217
216
|
}
|
|
218
217
|
}
|
|
219
218
|
|
|
@@ -221,29 +220,53 @@ function maxHeapify(heap, compare) {
|
|
|
221
220
|
* Get the first N elements of the sorted array efficiently.
|
|
222
221
|
* @template T
|
|
223
222
|
* @param {Array<T>} array
|
|
224
|
-
* @param {
|
|
225
|
-
* @param {
|
|
223
|
+
* @param {Object} $1
|
|
224
|
+
* @param {number} $1.N Number of elements to return; must be a nonnegative integer
|
|
225
|
+
* @param {Function=} $1.compare Sort function. Default is ascending sort.
|
|
226
|
+
* @param {boolean=} $1.unsorted If true, returns the final result in heap order, not sorted order, as an optimization.
|
|
227
|
+
* Default is false.
|
|
228
|
+
* @param {boolean=} $1.force If true, will force heap-based method for small N instead of more efficient "normal" way.
|
|
229
|
+
* Default is false.
|
|
230
|
+
* @param {boolean=} $1.mutate If true, will mutate the original array rather than copy it.
|
|
226
231
|
* @returns {Array<T>}
|
|
227
232
|
*/
|
|
228
|
-
export function sortN(
|
|
229
|
-
|
|
233
|
+
export function sortN(
|
|
234
|
+
array,
|
|
235
|
+
{ N, compare = ascending(), unsorted = false, force = false, mutate = false }
|
|
236
|
+
) {
|
|
237
|
+
if (!(N >= 0) || N % 1 !== 0) {
|
|
238
|
+
throw new Error("N must be a nonnegative integer")
|
|
239
|
+
}
|
|
240
|
+
if (N === 0) {
|
|
241
|
+
if (mutate) {
|
|
242
|
+
array.length = 0
|
|
243
|
+
return array
|
|
244
|
+
}
|
|
230
245
|
return []
|
|
231
246
|
}
|
|
232
247
|
if (N >= array.length) {
|
|
233
|
-
return [...array].sort(compare)
|
|
248
|
+
return (mutate ? array : [...array]).sort(compare)
|
|
234
249
|
}
|
|
235
|
-
if (array.length <= 100 && N / array.length >= 0.1) {
|
|
250
|
+
if (!force && array.length <= 100 && N / array.length >= 0.1) {
|
|
236
251
|
// seems to be faster to do it the "normal" way in this case
|
|
237
|
-
|
|
252
|
+
const sorted = (mutate ? array : [...array]).sort(compare)
|
|
253
|
+
sorted.length = N
|
|
254
|
+
return sorted
|
|
238
255
|
}
|
|
239
|
-
const heap = array.slice(0, N)
|
|
240
|
-
maxHeapify(heap, compare)
|
|
256
|
+
const heap = mutate ? array : array.slice(0, N)
|
|
257
|
+
maxHeapify(heap, compare, N)
|
|
241
258
|
for (let i = N; i < array.length; i++) {
|
|
242
259
|
const element = array[i]
|
|
243
260
|
if (compare(element, heap[0]) < 0) {
|
|
244
261
|
heap[0] = element
|
|
245
|
-
siftDown(heap, 0, compare)
|
|
262
|
+
siftDown(heap, 0, compare, N)
|
|
246
263
|
}
|
|
247
264
|
}
|
|
265
|
+
if (mutate) {
|
|
266
|
+
heap.length = N
|
|
267
|
+
}
|
|
268
|
+
if (unsorted) {
|
|
269
|
+
return heap
|
|
270
|
+
}
|
|
248
271
|
return heap.sort(compare)
|
|
249
272
|
}
|
package/src/array.test.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-restricted-syntax */
|
|
2
1
|
import { describe, expect, it, jest } from "@jest/globals"
|
|
3
2
|
|
|
4
3
|
const { chunk, unique, duplicates, ascending, descending, multilevel, sortN } = await import(
|
|
@@ -514,38 +513,121 @@ describe("multilevel", () => {
|
|
|
514
513
|
|
|
515
514
|
describe("sortN", () => {
|
|
516
515
|
it("returns empty array when N <= 0", () => {
|
|
517
|
-
expect(sortN([1, 2, 3], 0)).toEqual([])
|
|
518
|
-
expect(sortN([1, 2, 3], -5)).
|
|
516
|
+
expect(sortN([1, 2, 3], { N: 0, force: true })).toEqual([])
|
|
517
|
+
expect(() => sortN([1, 2, 3], { N: -5, force: true })).toThrow(
|
|
518
|
+
/N must be a nonnegative integer/u
|
|
519
|
+
)
|
|
520
|
+
expect(() => sortN([1, 2, 3], { N: 1.5, force: true })).toThrow(
|
|
521
|
+
/N must be a nonnegative integer/u
|
|
522
|
+
)
|
|
519
523
|
})
|
|
520
524
|
|
|
521
525
|
it("returns the entire array sorted when N >= array.length and does not mutate original", () => {
|
|
522
526
|
const arr = [3, 1, 2]
|
|
523
|
-
const result = sortN(arr, 10)
|
|
527
|
+
const result = sortN(arr, { N: 10, force: true })
|
|
524
528
|
expect(result).toEqual([1, 2, 3])
|
|
525
529
|
expect(arr).toEqual([3, 1, 2])
|
|
526
530
|
expect(result).not.toBe(arr)
|
|
527
531
|
})
|
|
528
532
|
|
|
529
|
-
it("returns the first N smallest elements (default ascending comparator)", () => {
|
|
530
|
-
expect(sortN([5, 1, 3, 2, 4], 3)).toEqual([1, 2, 3])
|
|
531
|
-
expect(sortN([3, 1, 3, 2, 2], 4)).toEqual([1, 2, 2, 3])
|
|
533
|
+
it("returns the first N smallest elements (default ascending comparator, heap path)", () => {
|
|
534
|
+
expect(sortN([5, 1, 3, 2, 4], { N: 3, force: true })).toEqual([1, 2, 3])
|
|
535
|
+
expect(sortN([3, 1, 3, 2, 2], { N: 4, force: true })).toEqual([1, 2, 2, 3])
|
|
532
536
|
})
|
|
533
537
|
|
|
534
538
|
it("respects a descending comparator (returns top N largest)", () => {
|
|
535
539
|
const arr = [5, 1, 3, 2, 4]
|
|
536
|
-
expect(sortN(arr, 2, descending())).toEqual([5, 4])
|
|
540
|
+
expect(sortN(arr, { N: 2, compare: descending(), force: true })).toEqual([5, 4])
|
|
537
541
|
})
|
|
538
542
|
|
|
539
543
|
it("works with key-based comparator and defers undefined/null values to the end", () => {
|
|
540
544
|
const arr = [{ v: 3 }, {}, { v: 1 }, { v: null }, { v: 2 }, { v: undefined }]
|
|
541
|
-
const out = sortN(arr, 3, ascending("v"))
|
|
545
|
+
const out = sortN(arr, { N: 3, compare: ascending("v"), force: true })
|
|
542
546
|
expect(out.map((o) => o.v)).toEqual([1, 2, 3])
|
|
543
547
|
})
|
|
544
548
|
|
|
545
|
-
it("does not mutate the original array when N < array.length", () => {
|
|
549
|
+
it("does not mutate the original array when N < array.length (heap path)", () => {
|
|
546
550
|
const arr = [5, 1, 3, 2, 4]
|
|
547
|
-
const out = sortN(arr, 3)
|
|
551
|
+
const out = sortN(arr, { N: 3, force: true })
|
|
548
552
|
expect(out).toEqual([1, 2, 3])
|
|
549
553
|
expect(arr).toEqual([5, 1, 3, 2, 4])
|
|
550
554
|
})
|
|
555
|
+
|
|
556
|
+
it("returns the N elements in heap order when unsorted=true (heap path)", () => {
|
|
557
|
+
expect(sortN([10, 1, 7, 3, 5], { N: 3, force: true, unsorted: true })).toEqual([5, 1, 3])
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
it("uses the 'normal' sort+truncate path when force=false and does not mutate original", () => {
|
|
561
|
+
const arr = [4, 2, 5, 1, 3] // small array and N/len >= 0.1 -> normal path
|
|
562
|
+
const out = sortN(arr, { N: 2 })
|
|
563
|
+
expect(out).toEqual([1, 2])
|
|
564
|
+
expect(arr).toEqual([4, 2, 5, 1, 3])
|
|
565
|
+
expect(out).not.toBe(arr)
|
|
566
|
+
})
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
describe("sortN mutate", () => {
|
|
570
|
+
it("returns empty array when N <= 0", () => {
|
|
571
|
+
const a = [1, 2, 3]
|
|
572
|
+
const result = sortN(a, { N: 0, force: true, mutate: true })
|
|
573
|
+
expect(a).toEqual([])
|
|
574
|
+
expect(result).toBe(a)
|
|
575
|
+
const b = [1, 2, 3]
|
|
576
|
+
expect(() => sortN(b, { N: -5, force: true, mutate: true })).toThrow(
|
|
577
|
+
/N must be a nonnegative integer/u
|
|
578
|
+
)
|
|
579
|
+
expect(() => sortN(b, { N: -1.5, force: true, mutate: true })).toThrow(
|
|
580
|
+
/N must be a nonnegative integer/u
|
|
581
|
+
)
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
it("returns the entire array sorted when N >= array.length", () => {
|
|
585
|
+
const array = [3, 1, 2]
|
|
586
|
+
const result = sortN(array, { N: 10, force: true, mutate: true })
|
|
587
|
+
expect(array).toEqual([1, 2, 3])
|
|
588
|
+
expect(result).toBe(array)
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
it("returns the first N smallest elements and mutates in-place (heap path)", () => {
|
|
592
|
+
const arr = [5, 1, 3, 2, 4]
|
|
593
|
+
const out = sortN(arr, { N: 3, force: true, mutate: true })
|
|
594
|
+
expect(out).toBe(arr)
|
|
595
|
+
expect(arr).toEqual([1, 2, 3])
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
it("respects a descending comparator and mutates in-place (heap path)", () => {
|
|
599
|
+
const arr = [5, 1, 3, 2, 4]
|
|
600
|
+
const out = sortN(arr, { N: 2, compare: descending(), force: true, mutate: true })
|
|
601
|
+
expect(out).toBe(arr)
|
|
602
|
+
expect(arr).toEqual([5, 4])
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
it("works with key-based comparator, mutating and truncating in-place (heap path)", () => {
|
|
606
|
+
const arr = [{ v: 3 }, {}, { v: 1 }, { v: null }, { v: 2 }, { v: undefined }]
|
|
607
|
+
const out = sortN(arr, { N: 3, compare: ascending("v"), force: true, mutate: true })
|
|
608
|
+
expect(out).toBe(arr)
|
|
609
|
+
expect(arr.length).toBe(3)
|
|
610
|
+
expect(out.map((o) => o.v)).toEqual([1, 2, 3])
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
it("mutates the original array when mutate=true and N < array.length (heap path)", () => {
|
|
614
|
+
const arr = [5, 1, 3, 2, 4]
|
|
615
|
+
const out = sortN(arr, { N: 3, force: true, mutate: true })
|
|
616
|
+
expect(out).toBe(arr)
|
|
617
|
+
expect(arr).toEqual([1, 2, 3])
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
it("returns the first N elements in heap order when unsorted=true (heap path, mutate=true)", () => {
|
|
621
|
+
const arr = [10, 1, 7, 3, 5]
|
|
622
|
+
const out = sortN(arr, { N: 3, force: true, unsorted: true, mutate: true })
|
|
623
|
+
expect(out).toBe(arr)
|
|
624
|
+
expect(arr).toEqual([5, 1, 3])
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
it("uses the 'normal' sort+truncate path when force=false and mutates in-place", () => {
|
|
628
|
+
const arr = [4, 2, 5, 1, 3] // small array and N/len >= 0.1 -> normal path
|
|
629
|
+
const out = sortN(arr, { N: 2, mutate: true })
|
|
630
|
+
expect(out).toBe(arr)
|
|
631
|
+
expect(arr).toEqual([1, 2])
|
|
632
|
+
})
|
|
551
633
|
})
|
package/src/find.test.js
CHANGED
package/src/math.test.js
CHANGED
|
@@ -248,7 +248,6 @@ describe("formatPlus", () => {
|
|
|
248
248
|
|
|
249
249
|
it("returns undefined for non-number, non-string input", () => {
|
|
250
250
|
expect(formatPlus(undefined)).toBeUndefined()
|
|
251
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
252
251
|
expect(formatPlus(null)).toBeUndefined()
|
|
253
252
|
expect(formatPlus({})).toBeUndefined()
|
|
254
253
|
expect(formatPlus([])).toBeUndefined()
|
|
@@ -323,7 +322,6 @@ describe("isNumber", () => {
|
|
|
323
322
|
it("returns false for non-number types", () => {
|
|
324
323
|
expect(isNumber("123")).toBe(false)
|
|
325
324
|
expect(isNumber(undefined)).toBe(false)
|
|
326
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
327
325
|
expect(isNumber(null)).toBe(false)
|
|
328
326
|
expect(isNumber({})).toBe(false)
|
|
329
327
|
expect(isNumber([])).toBe(false)
|
package/src/object.test.js
CHANGED
package/src/promise.test.js
CHANGED