@tim-code/my-util 0.5.5 → 0.5.7

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.5",
3
+ "version": "0.5.7",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "Tim Sprowl",
@@ -28,8 +28,8 @@
28
28
  ]
29
29
  },
30
30
  "devDependencies": {
31
- "@tim-code/eslint-config": "^1.3.3",
32
31
  "@jest/globals": "^29.7.0",
32
+ "@tim-code/eslint-config": "^1.3.3",
33
33
  "jest": "^29.7.0"
34
34
  },
35
35
  "jest": {
@@ -37,5 +37,8 @@
37
37
  },
38
38
  "publishConfig": {
39
39
  "access": "public"
40
+ },
41
+ "dependencies": {
42
+ "@tim-code/my-util": "^0.5.5"
40
43
  }
41
44
  }
package/src/array.js CHANGED
@@ -54,6 +54,50 @@ export function unique(array, { key } = {}) {
54
54
  return result
55
55
  }
56
56
 
57
+ /**
58
+ * Returns groups of duplicate elements in an array.
59
+ * Each group is an array of elements that share the same key or callback result.
60
+ * Only groups with more than one element are returned. Returns an empty array if no duplicates.
61
+ * @param {Array} array
62
+ * @param {Object} $1
63
+ * @param {string|number|Function=} $1.key
64
+ * If a function, calls the provided function on an element to get the value for grouping.
65
+ * If a string or number, uses element[key].
66
+ * If omitted, compares elements directly.
67
+ * @returns {Array<Array>}
68
+ */
69
+ export function duplicates(array, { key } = {}) {
70
+ const groups = new Map()
71
+
72
+ if (typeof key === "function") {
73
+ for (let i = 0; i < array.length; i++) {
74
+ const element = array[i]
75
+ const value = key(element, i, array)
76
+ if (!groups.has(value)) {
77
+ groups.set(value, [])
78
+ }
79
+ groups.get(value).push(element)
80
+ }
81
+ } else if (typeof key === "string" || typeof key === "number") {
82
+ for (const element of array) {
83
+ const value = element[key]
84
+ if (!groups.has(value)) {
85
+ groups.set(value, [])
86
+ }
87
+ groups.get(value).push(element)
88
+ }
89
+ } else {
90
+ for (const element of array) {
91
+ if (!groups.has(element)) {
92
+ groups.set(element, [])
93
+ }
94
+ groups.get(element).push(element)
95
+ }
96
+ }
97
+ const results = [...groups.values()].filter((group) => group.length > 1)
98
+ return results
99
+ }
100
+
57
101
  // sorts undefined and null to the end if applicable
58
102
  function compareUndefinedNull(a, b) {
59
103
  if (b === undefined || b === null) {
@@ -141,3 +185,65 @@ export function multilevel(...comparators) {
141
185
  return 0
142
186
  }
143
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 {number} N
225
+ * @param {Function} compare
226
+ * @returns {Array<T>}
227
+ */
228
+ export function sortN(array, N, compare = ascending()) {
229
+ if (N <= 0) {
230
+ return []
231
+ }
232
+ if (N >= array.length) {
233
+ return [...array].sort(compare)
234
+ }
235
+ if (array.length <= 100 && N / array.length >= 0.1) {
236
+ // seems to be faster to do it the "normal" way in this case
237
+ return [...array].sort(compare).slice(0, N)
238
+ }
239
+ const heap = array.slice(0, N)
240
+ maxHeapify(heap, compare)
241
+ for (let i = N; i < array.length; i++) {
242
+ const element = array[i]
243
+ if (compare(element, heap[0]) < 0) {
244
+ heap[0] = element
245
+ siftDown(heap, 0, compare)
246
+ }
247
+ }
248
+ return heap.sort(compare)
249
+ }
package/src/array.test.js CHANGED
@@ -1,7 +1,9 @@
1
1
  /* eslint-disable no-restricted-syntax */
2
2
  import { describe, expect, it, jest } from "@jest/globals"
3
3
 
4
- const { chunk, unique, ascending, descending, multilevel } = await import("./array.js")
4
+ const { chunk, unique, duplicates, ascending, descending, multilevel, sortN } = await import(
5
+ "./array.js"
6
+ )
5
7
 
6
8
  describe("chunk", () => {
7
9
  it("splits array into chunks of specified size", () => {
@@ -162,6 +164,169 @@ describe("unique", () => {
162
164
  })
163
165
  })
164
166
 
167
+ describe("duplicates", () => {
168
+ it("returns empty array if there are no duplicates", () => {
169
+ expect(duplicates([1, 2, 3])).toEqual([])
170
+ expect(duplicates([], {})).toEqual([])
171
+ })
172
+
173
+ it("returns groups of duplicate primitives", () => {
174
+ expect(duplicates([1, 2, 1, 3, 2, 1])).toEqual([
175
+ [1, 1, 1],
176
+ [2, 2],
177
+ ])
178
+ expect(duplicates(["a", "b", "a", "c", "b"])).toEqual([
179
+ ["a", "a"],
180
+ ["b", "b"],
181
+ ])
182
+ })
183
+
184
+ it("returns groups of duplicate objects by reference", () => {
185
+ const a = {}
186
+ const b = {}
187
+ expect(duplicates([a, b, a, b, a])).toEqual([
188
+ [a, a, a],
189
+ [b, b],
190
+ ])
191
+ })
192
+
193
+ it("returns groups of duplicates by key (string)", () => {
194
+ const arr = [
195
+ { id: 1, name: "a" },
196
+ { id: 2, name: "b" },
197
+ { id: 1, name: "c" },
198
+ { id: 3, name: "d" },
199
+ { id: 2, name: "e" },
200
+ { id: 1, name: "f" },
201
+ ]
202
+ expect(duplicates(arr, { key: "id" })).toEqual([
203
+ [
204
+ { id: 1, name: "a" },
205
+ { id: 1, name: "c" },
206
+ { id: 1, name: "f" },
207
+ ],
208
+ [
209
+ { id: 2, name: "b" },
210
+ { id: 2, name: "e" },
211
+ ],
212
+ ])
213
+ })
214
+
215
+ it("returns groups of duplicates by key (number)", () => {
216
+ const arr = [
217
+ { 0: "a", v: 1 },
218
+ { 0: "b", v: 2 },
219
+ { 0: "a", v: 3 },
220
+ { 0: "c", v: 4 },
221
+ { 0: "b", v: 5 },
222
+ ]
223
+ expect(duplicates(arr, { key: 0 })).toEqual([
224
+ [
225
+ { 0: "a", v: 1 },
226
+ { 0: "a", v: 3 },
227
+ ],
228
+ [
229
+ { 0: "b", v: 2 },
230
+ { 0: "b", v: 5 },
231
+ ],
232
+ ])
233
+ })
234
+
235
+ it("returns groups of duplicates by function", () => {
236
+ const arr = [
237
+ { id: 1, name: "a" },
238
+ { id: 2, name: "b" },
239
+ { id: 1, name: "c" },
240
+ { id: 3, name: "d" },
241
+ { id: 2, name: "e" },
242
+ { id: 1, name: "f" },
243
+ ]
244
+ expect(
245
+ duplicates(arr, {
246
+ key: (el) => el.id % 2, // group by odd/even id
247
+ })
248
+ ).toEqual([
249
+ [
250
+ { id: 1, name: "a" },
251
+ { id: 1, name: "c" },
252
+ { id: 3, name: "d" },
253
+ { id: 1, name: "f" },
254
+ ],
255
+ [
256
+ { id: 2, name: "b" },
257
+ { id: 2, name: "e" },
258
+ ],
259
+ ])
260
+ })
261
+
262
+ it("returns groups of duplicates by function using index and array", () => {
263
+ const arr = ["a", "b", "c", "a", "c"]
264
+ expect(
265
+ duplicates(arr, {
266
+ key: (el, i, array) => array.indexOf(el), // group by first occurrence index
267
+ })
268
+ ).toEqual([
269
+ ["a", "a"],
270
+ ["c", "c"],
271
+ ])
272
+ })
273
+
274
+ it("returns groups of duplicates by key when some elements lack the key", () => {
275
+ const arr = [{ id: 1 }, {}, { id: 1 }, { id: 2 }, {}, { id: 2 }]
276
+ expect(duplicates(arr, { key: "id" })).toEqual([
277
+ [{ id: 1 }, { id: 1 }],
278
+ [{}, {}],
279
+ [{ id: 2 }, { id: 2 }],
280
+ ])
281
+ })
282
+
283
+ it("returns groups of duplicates by function when function returns undefined/null", () => {
284
+ const arr = [{ id: 1 }, {}, { id: 2 }, { id: null }, {}, { id: null }]
285
+ expect(
286
+ duplicates(arr, {
287
+ key: (el) => el.id,
288
+ })
289
+ ).toEqual([
290
+ [{}, {}],
291
+ [{ id: null }, { id: null }],
292
+ ])
293
+ })
294
+
295
+ it("returns groups of duplicates by key when key value is undefined/null", () => {
296
+ const arr = [
297
+ { id: 1 },
298
+ { id: undefined },
299
+ { id: 2 },
300
+ { id: null },
301
+ { id: undefined },
302
+ { id: null },
303
+ ]
304
+ expect(duplicates(arr, { key: "id" })).toEqual([
305
+ [{ id: undefined }, { id: undefined }],
306
+ [{ id: null }, { id: null }],
307
+ ])
308
+ })
309
+
310
+ it("returns empty array if all elements are unique by key or function", () => {
311
+ const arr = [{ id: 1 }, { id: 2 }, { id: 3 }]
312
+ expect(duplicates(arr, { key: "id" })).toEqual([])
313
+ expect(
314
+ duplicates(arr, {
315
+ key: (el) => el.id,
316
+ })
317
+ ).toEqual([])
318
+ })
319
+
320
+ it("returns empty array for empty input with key or function", () => {
321
+ expect(duplicates([], { key: "id" })).toEqual([])
322
+ expect(
323
+ duplicates([], {
324
+ key: (el) => el,
325
+ })
326
+ ).toEqual([])
327
+ })
328
+ })
329
+
165
330
  describe("ascending", () => {
166
331
  it("sorts primitives ascending, undefined/null at end", () => {
167
332
  const arr = [undefined, null, 3, 1, 2]
@@ -346,3 +511,41 @@ describe("multilevel", () => {
346
511
  expect(cmp("a", "b")).toBe(0)
347
512
  })
348
513
  })
514
+
515
+ describe("sortN", () => {
516
+ it("returns empty array when N <= 0", () => {
517
+ expect(sortN([1, 2, 3], 0)).toEqual([])
518
+ expect(sortN([1, 2, 3], -5)).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, 10)
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], 3)).toEqual([1, 2, 3])
531
+ expect(sortN([3, 1, 3, 2, 2], 4)).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, 2, descending())).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, 3, ascending("v"))
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, 3)
548
+ expect(out).toEqual([1, 2, 3])
549
+ expect(arr).toEqual([5, 1, 3, 2, 4])
550
+ })
551
+ })