@tim-code/my-util 0.5.8 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tim-code/my-util",
3
- "version": "0.5.8",
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.3.3",
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 < n && compare(heap[left], heap[largest]) > 0) {
195
+ if (left < length && compare(heap[left], heap[largest]) > 0) {
197
196
  largest = left
198
197
  }
199
- if (right < n && compare(heap[right], heap[largest]) > 0) {
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 = (heap.length >>> 1) - 1; i >= 0; 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
 
@@ -222,34 +221,50 @@ function maxHeapify(heap, compare) {
222
221
  * @template T
223
222
  * @param {Array<T>} array
224
223
  * @param {Object} $1
225
- * @param {number} $1.N Number of elements to return
224
+ * @param {number} $1.N Number of elements to return; must be a nonnegative integer
226
225
  * @param {Function=} $1.compare Sort function. Default is ascending sort.
227
226
  * @param {boolean=} $1.unsorted If true, returns the final result in heap order, not sorted order, as an optimization.
228
227
  * Default is false.
229
228
  * @param {boolean=} $1.force If true, will force heap-based method for small N instead of more efficient "normal" way.
230
229
  * Default is false.
230
+ * @param {boolean=} $1.mutate If true, will mutate the original array rather than copy it.
231
231
  * @returns {Array<T>}
232
232
  */
233
- export function sortN(array, { N, compare = ascending(), unsorted = false, force = false }) {
234
- if (!(N > 0)) {
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
+ }
235
245
  return []
236
246
  }
237
247
  if (N >= array.length) {
238
- return [...array].sort(compare)
248
+ return (mutate ? array : [...array]).sort(compare)
239
249
  }
240
250
  if (!force && array.length <= 100 && N / array.length >= 0.1) {
241
251
  // seems to be faster to do it the "normal" way in this case
242
- return [...array].sort(compare).slice(0, N)
252
+ const sorted = (mutate ? array : [...array]).sort(compare)
253
+ sorted.length = N
254
+ return sorted
243
255
  }
244
- const heap = array.slice(0, N)
245
- maxHeapify(heap, compare)
256
+ const heap = mutate ? array : array.slice(0, N)
257
+ maxHeapify(heap, compare, N)
246
258
  for (let i = N; i < array.length; i++) {
247
259
  const element = array[i]
248
260
  if (compare(element, heap[0]) < 0) {
249
261
  heap[0] = element
250
- siftDown(heap, 0, compare)
262
+ siftDown(heap, 0, compare, N)
251
263
  }
252
264
  }
265
+ if (mutate) {
266
+ heap.length = N
267
+ }
253
268
  if (unsorted) {
254
269
  return heap
255
270
  }
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(
@@ -515,7 +514,12 @@ describe("multilevel", () => {
515
514
  describe("sortN", () => {
516
515
  it("returns empty array when N <= 0", () => {
517
516
  expect(sortN([1, 2, 3], { N: 0, force: true })).toEqual([])
518
- expect(sortN([1, 2, 3], { N: -5, 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", () => {
@@ -526,7 +530,7 @@ describe("sortN", () => {
526
530
  expect(result).not.toBe(arr)
527
531
  })
528
532
 
529
- it("returns the first N smallest elements (default ascending comparator)", () => {
533
+ it("returns the first N smallest elements (default ascending comparator, heap path)", () => {
530
534
  expect(sortN([5, 1, 3, 2, 4], { N: 3, force: true })).toEqual([1, 2, 3])
531
535
  expect(sortN([3, 1, 3, 2, 2], { N: 4, force: true })).toEqual([1, 2, 2, 3])
532
536
  })
@@ -542,14 +546,88 @@ describe("sortN", () => {
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
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
  })
551
555
 
552
- it("returns the first N smallest elements (default ascending comparator)", () => {
556
+ it("returns the N elements in heap order when unsorted=true (heap path)", () => {
553
557
  expect(sortN([10, 1, 7, 3, 5], { N: 3, force: true, unsorted: true })).toEqual([5, 1, 3])
554
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
+ })
555
633
  })
package/src/find.test.js CHANGED
@@ -1,4 +1,3 @@
1
- /* eslint-disable no-restricted-syntax */
2
1
  import { describe, expect, it } from "@jest/globals"
3
2
 
4
3
  const {
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)
@@ -1,4 +1,3 @@
1
- /* eslint-disable no-restricted-syntax */
2
1
  import { jest } from "@jest/globals"
3
2
  import {
4
3
  deepCopy,
@@ -1,4 +1,3 @@
1
- /* eslint-disable no-restricted-syntax */
2
1
  /* eslint-disable prefer-promise-reject-errors */
3
2
  import { jest } from "@jest/globals"
4
3