@tim-code/my-util 0.5.6 → 0.5.8

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.6",
3
+ "version": "0.5.8",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "Tim Sprowl",
package/src/array.js CHANGED
@@ -185,3 +185,73 @@ export function multilevel(...comparators) {
185
185
  return 0
186
186
  }
187
187
  }
188
+
189
+ function siftDown(heap, i, compare) {
190
+ const n = heap.length
191
+ // eslint-disable-next-line no-constant-condition
192
+ while (true) {
193
+ const left = 2 * i + 1
194
+ const right = left + 1
195
+ let largest = i
196
+ if (left < n && compare(heap[left], heap[largest]) > 0) {
197
+ largest = left
198
+ }
199
+ if (right < n && compare(heap[right], heap[largest]) > 0) {
200
+ largest = right
201
+ }
202
+ if (largest === i) {
203
+ break
204
+ }
205
+ const swap = heap[largest]
206
+ heap[largest] = heap[i]
207
+ heap[i] = swap
208
+ i = largest
209
+ }
210
+ }
211
+
212
+ function maxHeapify(heap, compare) {
213
+ // (heap.length >>> 1) is equivalent to Math.floor(heap.length / 2)
214
+ // eslint-disable-next-line no-bitwise
215
+ for (let i = (heap.length >>> 1) - 1; i >= 0; i--) {
216
+ siftDown(heap, i, compare)
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Get the first N elements of the sorted array efficiently.
222
+ * @template T
223
+ * @param {Array<T>} array
224
+ * @param {Object} $1
225
+ * @param {number} $1.N Number of elements to return
226
+ * @param {Function=} $1.compare Sort function. Default is ascending sort.
227
+ * @param {boolean=} $1.unsorted If true, returns the final result in heap order, not sorted order, as an optimization.
228
+ * Default is false.
229
+ * @param {boolean=} $1.force If true, will force heap-based method for small N instead of more efficient "normal" way.
230
+ * Default is false.
231
+ * @returns {Array<T>}
232
+ */
233
+ export function sortN(array, { N, compare = ascending(), unsorted = false, force = false }) {
234
+ if (!(N > 0)) {
235
+ return []
236
+ }
237
+ if (N >= array.length) {
238
+ return [...array].sort(compare)
239
+ }
240
+ if (!force && array.length <= 100 && N / array.length >= 0.1) {
241
+ // seems to be faster to do it the "normal" way in this case
242
+ return [...array].sort(compare).slice(0, N)
243
+ }
244
+ const heap = array.slice(0, N)
245
+ maxHeapify(heap, compare)
246
+ for (let i = N; i < array.length; i++) {
247
+ const element = array[i]
248
+ if (compare(element, heap[0]) < 0) {
249
+ heap[0] = element
250
+ siftDown(heap, 0, compare)
251
+ }
252
+ }
253
+ if (unsorted) {
254
+ return heap
255
+ }
256
+ return heap.sort(compare)
257
+ }
package/src/array.test.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable no-restricted-syntax */
2
2
  import { describe, expect, it, jest } from "@jest/globals"
3
3
 
4
- const { chunk, unique, duplicates, ascending, descending, multilevel } = await import(
4
+ const { chunk, unique, duplicates, ascending, descending, multilevel, sortN } = await import(
5
5
  "./array.js"
6
6
  )
7
7
 
@@ -511,3 +511,45 @@ describe("multilevel", () => {
511
511
  expect(cmp("a", "b")).toBe(0)
512
512
  })
513
513
  })
514
+
515
+ describe("sortN", () => {
516
+ it("returns empty array when N <= 0", () => {
517
+ expect(sortN([1, 2, 3], { N: 0, force: true })).toEqual([])
518
+ expect(sortN([1, 2, 3], { N: -5, force: true })).toEqual([])
519
+ })
520
+
521
+ it("returns the entire array sorted when N >= array.length and does not mutate original", () => {
522
+ const arr = [3, 1, 2]
523
+ const result = sortN(arr, { N: 10, force: true })
524
+ expect(result).toEqual([1, 2, 3])
525
+ expect(arr).toEqual([3, 1, 2])
526
+ expect(result).not.toBe(arr)
527
+ })
528
+
529
+ it("returns the first N smallest elements (default ascending comparator)", () => {
530
+ expect(sortN([5, 1, 3, 2, 4], { N: 3, force: true })).toEqual([1, 2, 3])
531
+ expect(sortN([3, 1, 3, 2, 2], { N: 4, force: true })).toEqual([1, 2, 2, 3])
532
+ })
533
+
534
+ it("respects a descending comparator (returns top N largest)", () => {
535
+ const arr = [5, 1, 3, 2, 4]
536
+ expect(sortN(arr, { N: 2, compare: descending(), force: true })).toEqual([5, 4])
537
+ })
538
+
539
+ it("works with key-based comparator and defers undefined/null values to the end", () => {
540
+ const arr = [{ v: 3 }, {}, { v: 1 }, { v: null }, { v: 2 }, { v: undefined }]
541
+ const out = sortN(arr, { N: 3, compare: ascending("v"), force: true })
542
+ expect(out.map((o) => o.v)).toEqual([1, 2, 3])
543
+ })
544
+
545
+ it("does not mutate the original array when N < array.length", () => {
546
+ const arr = [5, 1, 3, 2, 4]
547
+ const out = sortN(arr, { N: 3, force: true })
548
+ expect(out).toEqual([1, 2, 3])
549
+ expect(arr).toEqual([5, 1, 3, 2, 4])
550
+ })
551
+
552
+ it("returns the first N smallest elements (default ascending comparator)", () => {
553
+ expect(sortN([10, 1, 7, 3, 5], { N: 3, force: true, unsorted: true })).toEqual([5, 1, 3])
554
+ })
555
+ })