@tim-code/my-util 0.4.8 → 0.4.10

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.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "Tim Sprowl",
package/src/find.js CHANGED
@@ -271,39 +271,83 @@ export function findMax(array, { key, cutoff = -Infinity } = {}) {
271
271
 
272
272
  /**
273
273
  * Find the first truthy value in an array.
274
- * Supports "from" and "to" being reversed to "find last truthy".
274
+ * Supports "from" and "to"/"until" being reversed to "find last truthy" if "reverse" is true.
275
275
  * @template T
276
276
  * @param {Array<T>} array
277
277
  * @param {Object} $1
278
278
  * @param {string|Function=} $1.key Specifies an alternative to using each element as the value.
279
279
  * If string, then accesses each element at that key to get value.
280
280
  * If function, then calls the callback on each element to get value.
281
- * @param {number=} $1.from Numeric index to start from: inclusive, defaults to 0
281
+ * @param {number=} $1.from Numeric index to start from: inclusive, defaults to `0`
282
282
  * @param {number=} $1.until Numeric index to end at: exclusive, defaults to `array.length`
283
+ * @param {number=} $1.to Numeric index to end at: inclusive, defaults to `array.length - 1`; takes precedence over "until"
284
+ * @param {boolean=} $1.reverse If true, looks through the array in reverse. Default is false.
285
+ * When true, findTruthy() will still start from "from" and end at "to"/"until".
286
+ * When true, changes the default values of "from", "until", "to" to `array.length - 1`, `-1`, `0`.
283
287
  * @returns {T}
284
288
  */
285
- export function findTruthy(array, { key, from = 0, until = array.length } = {}) {
289
+ // eslint-disable-next-line complexity
290
+ export function findTruthy(
291
+ array,
292
+ {
293
+ key,
294
+ reverse = false,
295
+ from = reverse ? array.length - 1 : 0,
296
+ until = reverse ? -1 : array.length,
297
+ to = reverse ? until + 1 : until - 1,
298
+ } = {}
299
+ ) {
286
300
  if (typeof key === "function") {
287
- for (let i = from; i < until; i++) {
288
- const element = array[i]
289
- const value = key(element, i, array)
290
- if (value) {
291
- return element
301
+ if (reverse) {
302
+ for (let i = from; i >= to; i--) {
303
+ const element = array[i]
304
+ const value = key(element, i, array)
305
+ if (value) {
306
+ return element
307
+ }
308
+ }
309
+ } else {
310
+ for (let i = from; i <= to; i++) {
311
+ const element = array[i]
312
+ const value = key(element, i, array)
313
+ if (value) {
314
+ return element
315
+ }
292
316
  }
293
317
  }
294
318
  } else if (typeof key === "number" || typeof key === "string") {
295
- for (let i = from; i < until; i++) {
296
- const element = array[i]
297
- const value = element[key]
298
- if (value) {
299
- return element
319
+ if (reverse) {
320
+ for (let i = from; i >= to; i--) {
321
+ const element = array[i]
322
+ const value = element[key]
323
+ if (value) {
324
+ return element
325
+ }
326
+ }
327
+ } else {
328
+ for (let i = from; i <= to; i++) {
329
+ const element = array[i]
330
+ const value = element[key]
331
+ if (value) {
332
+ return element
333
+ }
300
334
  }
301
335
  }
302
336
  } else {
303
- for (let i = from; i < until; i++) {
304
- const value = array[i]
305
- if (value) {
306
- return value
337
+ // eslint-disable-next-line no-lonely-if
338
+ if (reverse) {
339
+ for (let i = from; i >= to; i--) {
340
+ const value = array[i]
341
+ if (value) {
342
+ return value
343
+ }
344
+ }
345
+ } else {
346
+ for (let i = from; i <= to; i++) {
347
+ const value = array[i]
348
+ if (value) {
349
+ return value
350
+ }
307
351
  }
308
352
  }
309
353
  }
package/src/find.test.js CHANGED
@@ -323,4 +323,116 @@ describe("findTruthy", () => {
323
323
  expect(findTruthy([0, 1, 2], { from: 2, until: 2 })).toBeUndefined()
324
324
  expect(findTruthy([0, 1, 2], { from: 3, until: 2 })).toBeUndefined()
325
325
  })
326
+
327
+ it("respects the new 'to' option (inclusive end)", () => {
328
+ const arr = [0, 1, 2, 3]
329
+ expect(findTruthy(arr, { from: 1, to: 2 })).toBe(1)
330
+ expect(findTruthy(arr, { from: 2, to: 3 })).toBe(2)
331
+ expect(findTruthy(arr, { from: 2, to: 2 })).toBe(2)
332
+ expect(findTruthy(arr, { from: 2, to: 1 })).toBeUndefined()
333
+ expect(findTruthy(arr, { from: 0, to: 0 })).toBeUndefined()
334
+ expect(findTruthy(arr, { from: 1, to: 1 })).toBe(1)
335
+ })
336
+
337
+ it("prefers 'to' over 'until' if both are given", () => {
338
+ const arr = [0, 1, 2, 3]
339
+ // 'to' = 1, so only index 1 is checked, even though until=4
340
+ expect(findTruthy(arr, { from: 1, until: 4, to: 1 })).toBe(1)
341
+ // 'to' = 2, so indices 1 and 2 checked, even though until=2 (which would skip 2)
342
+ expect(findTruthy(arr, { from: 1, until: 2, to: 2 })).toBe(1)
343
+ })
344
+
345
+ it("works with key as function and 'to'", () => {
346
+ const arr = [{ v: 0 }, { v: 0 }, { v: 2 }, { v: 3 }]
347
+ expect(findTruthy(arr, { key: (e) => e.v, from: 1, to: 2 })).toEqual({ v: 2 })
348
+ expect(findTruthy(arr, { key: (e) => e.v, from: 1, to: 1 })).toBeUndefined()
349
+ })
350
+
351
+ it("works with key as string and 'to'", () => {
352
+ const arr = [{ x: 0 }, { x: 2 }, { x: 0 }]
353
+ expect(findTruthy(arr, { key: "x", from: 0, to: 1 })).toEqual({ x: 2 })
354
+ expect(findTruthy(arr, { key: "x", from: 1, to: 1 })).toEqual({ x: 2 })
355
+ expect(findTruthy(arr, { key: "x", from: 2, to: 1 })).toBeUndefined()
356
+ })
357
+
358
+ it("works with key as number and 'to'", () => {
359
+ const arr = [[0], [2], [0]]
360
+ expect(findTruthy(arr, { key: 0, from: 0, to: 1 })).toEqual([2])
361
+ expect(findTruthy(arr, { key: 0, from: 1, to: 1 })).toEqual([2])
362
+ expect(findTruthy(arr, { key: 0, from: 2, to: 1 })).toBeUndefined()
363
+ })
364
+
365
+ // New tests for reverse mode and its interaction with from, to, until
366
+ it("finds last truthy value with reverse=true and default from/to", () => {
367
+ const arr = [0, 1, 2, 3, 0]
368
+ expect(findTruthy(arr, { reverse: true })).toBe(3)
369
+ })
370
+
371
+ it("finds last truthy value with reverse=true and custom from/to", () => {
372
+ const arr = [0, 1, 2, 3, 4]
373
+ // from=3, to=1 (reverse), should check indices 3,2,1
374
+ expect(findTruthy(arr, { reverse: true, from: 3, to: 1 })).toBe(3)
375
+ // from=4, to=2, should check 4,3,2
376
+ expect(findTruthy(arr, { reverse: true, from: 4, to: 2 })).toBe(4)
377
+ // from=2, to=0, should check 2,1,0
378
+ expect(findTruthy(arr, { reverse: true, from: 2, to: 0 })).toBe(2)
379
+ // from=2, to=2, should check only 2
380
+ expect(findTruthy(arr, { reverse: true, from: 2, to: 2 })).toBe(2)
381
+ // from=2, to=3, should check 2,1,0 (since to > from, loop doesn't run)
382
+ expect(findTruthy(arr, { reverse: true, from: 2, to: 3 })).toBeUndefined()
383
+ })
384
+
385
+ it("finds last truthy value with reverse=true and until", () => {
386
+ const arr = [0, 1, 2, 3, 0]
387
+ // until=1, so to=until+1=2, from=4, should check 4,3,2
388
+ expect(findTruthy(arr, { reverse: true, until: 1 })).toBe(3)
389
+ expect(findTruthy(arr, { reverse: true, until: 2 })).toBe(3)
390
+ expect(findTruthy(arr, { reverse: true, until: 3 })).toBe(undefined)
391
+ })
392
+
393
+ it("finds last truthy value with reverse=true and key as function", () => {
394
+ const arr = [{ v: 0 }, { v: 2 }, { v: 0 }]
395
+ expect(findTruthy(arr, { key: (e) => e.v, reverse: true })).toEqual({ v: 2 })
396
+ })
397
+
398
+ it("finds last truthy value with reverse=true and key as string", () => {
399
+ const arr = [{ x: 0 }, { x: 2 }, { x: 0 }]
400
+ expect(findTruthy(arr, { key: "x", reverse: true })).toEqual({ x: 2 })
401
+ })
402
+
403
+ it("finds last truthy value with reverse=true and key as number", () => {
404
+ const arr = [[0], [2], [0]]
405
+ expect(findTruthy(arr, { key: 0, reverse: true })).toEqual([2])
406
+ })
407
+
408
+ it("returns undefined if no truthy value in reverse mode", () => {
409
+ expect(findTruthy([0, 0, 0], { reverse: true })).toBeUndefined()
410
+ })
411
+
412
+ it("returns undefined if reverse=true and from < to", () => {
413
+ expect(findTruthy([0, 1, 2], { reverse: true, from: 0, to: 2 })).toBeUndefined()
414
+ })
415
+
416
+ it("reverse=true with custom from/to and key as function", () => {
417
+ const arr = [{ v: 0 }, { v: 2 }, { v: 0 }, { v: 3 }]
418
+ expect(findTruthy(arr, { key: (e) => e.v, reverse: true, from: 2, to: 1 })).toEqual({
419
+ v: 2,
420
+ })
421
+ expect(findTruthy(arr, { key: (e) => e.v, reverse: true, from: 1, to: 1 })).toEqual({
422
+ v: 2,
423
+ })
424
+ expect(findTruthy(arr, { key: (e) => e.v, reverse: true, from: 1, to: 2 })).toBeUndefined()
425
+ })
426
+
427
+ it("reverse=true with custom from/to and key as string", () => {
428
+ const arr = [{ x: 0 }, { x: 2 }, { x: 0 }]
429
+ expect(findTruthy(arr, { key: "x", reverse: true, from: 1, to: 1 })).toEqual({ x: 2 })
430
+ expect(findTruthy(arr, { key: "x", reverse: true, from: 1, to: 2 })).toBeUndefined()
431
+ })
432
+
433
+ it("reverse=true with custom from/to and key as number", () => {
434
+ const arr = [[0], [2], [0]]
435
+ expect(findTruthy(arr, { key: 0, reverse: true, from: 1, to: 1 })).toEqual([2])
436
+ expect(findTruthy(arr, { key: 0, reverse: true, from: 1, to: 2 })).toBeUndefined()
437
+ })
326
438
  })
package/src/object.js CHANGED
@@ -89,3 +89,37 @@ export function deepMerge(target, ...sources) {
89
89
  }
90
90
  return target
91
91
  }
92
+
93
+ /**
94
+ * Deeply compares two values to determine if they are equal.
95
+ * Objects and arrays are compared recursively by their properties and elements.
96
+ * Primitives are compared with strict equality.
97
+ * Caveats:
98
+ * Does not check class: [1] is considered equal to {0: 1}.
99
+ * Any Symbol keys in the arguments are ignored (Object.keys only returns string keys).
100
+ * @param {any} a The first value to compare.
101
+ * @param {any} b The second value to compare.
102
+ * @returns {boolean} True if the values are deeply equal, false otherwise.
103
+ */
104
+ export function deepEqual(a, b) {
105
+ if (a === b) {
106
+ return true
107
+ }
108
+ if (typeof a !== "object" || typeof b !== "object" || !a || !b) {
109
+ return false
110
+ }
111
+ const keysA = Object.keys(a)
112
+ const keysB = Object.keys(b)
113
+ if (keysA.length !== keysB.length) {
114
+ return false
115
+ }
116
+ for (const key of keysA) {
117
+ if (!Object.hasOwn(b, key)) {
118
+ return false
119
+ }
120
+ if (!deepEqual(a[key], b[key])) {
121
+ return false
122
+ }
123
+ }
124
+ return true
125
+ }
@@ -1,6 +1,13 @@
1
1
  /* eslint-disable no-restricted-syntax */
2
2
  import { jest } from "@jest/globals"
3
- import { deepMerge, deleteUndefinedValues, like, mutateValues, via } from "./object.js"
3
+ import {
4
+ deepEqual,
5
+ deepMerge,
6
+ deleteUndefinedValues,
7
+ like,
8
+ mutateValues,
9
+ via,
10
+ } from "./object.js"
4
11
 
5
12
  describe("mutateValues", () => {
6
13
  it("mutates values in the object using the callback", () => {
@@ -257,3 +264,106 @@ describe("deepMerge", () => {
257
264
  expect(result).toBe(target)
258
265
  })
259
266
  })
267
+
268
+ describe("deepEqual", () => {
269
+ it("returns true for strictly equal primitives", () => {
270
+ expect(deepEqual(1, 1)).toBe(true)
271
+ expect(deepEqual("foo", "foo")).toBe(true)
272
+ expect(deepEqual(true, true)).toBe(true)
273
+ expect(deepEqual(null, null)).toBe(true)
274
+ expect(deepEqual(undefined, undefined)).toBe(true)
275
+ })
276
+
277
+ it("returns false for different primitives", () => {
278
+ expect(deepEqual(1, 2)).toBe(false)
279
+ expect(deepEqual("foo", "bar")).toBe(false)
280
+ expect(deepEqual(true, false)).toBe(false)
281
+ expect(deepEqual(null, undefined)).toBe(false)
282
+ })
283
+
284
+ it("returns true for deeply equal objects", () => {
285
+ expect(deepEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true)
286
+ expect(deepEqual({ a: { b: 2 } }, { a: { b: 2 } })).toBe(true)
287
+ expect(deepEqual({}, {})).toBe(true)
288
+ })
289
+
290
+ it("returns false for objects with different keys or values", () => {
291
+ expect(deepEqual({ a: 1 }, { a: 2 })).toBe(false)
292
+ expect(deepEqual({ a: 1 }, { b: 1 })).toBe(false)
293
+ expect(deepEqual({ a: 1 }, {})).toBe(false)
294
+ expect(deepEqual({}, { a: 1 })).toBe(false)
295
+ })
296
+
297
+ it("returns true for deeply equal arrays", () => {
298
+ expect(deepEqual([1, 2, 3], [1, 2, 3])).toBe(true)
299
+ expect(deepEqual([], [])).toBe(true)
300
+ expect(deepEqual([[1], [2]], [[1], [2]])).toBe(true)
301
+ })
302
+
303
+ it("returns false for arrays with different elements or lengths", () => {
304
+ expect(deepEqual([1, 2], [1, 2, 3])).toBe(false)
305
+ expect(deepEqual([1, 2, 3], [1, 2])).toBe(false)
306
+ expect(deepEqual([1, 2, 3], [3, 2, 1])).toBe(false)
307
+ })
308
+
309
+ it("returns true if one is array and one is object", () => {
310
+ expect(deepEqual([1, 2], { 0: 1, 1: 2 })).toBe(true)
311
+ expect(deepEqual({ 0: 1, 1: 2 }, [1, 2])).toBe(true)
312
+ })
313
+
314
+ it("returns true for objects with same keys in different order", () => {
315
+ expect(deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true)
316
+ })
317
+
318
+ it("returns true for nested objects and arrays", () => {
319
+ const a = { foo: [1, { bar: 2 }], baz: { qux: [3] } }
320
+ const b = { foo: [1, { bar: 2 }], baz: { qux: [3] } }
321
+ expect(deepEqual(a, b)).toBe(true)
322
+ })
323
+
324
+ it("returns false for nested difference", () => {
325
+ const a = { foo: [1, { bar: 2 }], baz: { qux: [3] } }
326
+ const b = { foo: [1, { bar: 3 }], baz: { qux: [3] } }
327
+ expect(deepEqual(a, b)).toBe(false)
328
+ })
329
+
330
+ it("returns false if keys differ in nested objects", () => {
331
+ expect(deepEqual({ a: { b: 1 } }, { a: { c: 1 } })).toBe(false)
332
+ })
333
+
334
+ it("returns false if one is null or undefined and the other is object", () => {
335
+ expect(deepEqual(null, {})).toBe(false)
336
+ expect(deepEqual({}, null)).toBe(false)
337
+ expect(deepEqual(undefined, {})).toBe(false)
338
+ expect(deepEqual({}, undefined)).toBe(false)
339
+ })
340
+
341
+ it("returns true for self-references (same object)", () => {
342
+ const obj = { a: 1 }
343
+ expect(deepEqual(obj, obj)).toBe(true)
344
+ const arr = [1, 2]
345
+ expect(deepEqual(arr, arr)).toBe(true)
346
+ })
347
+
348
+ it("returns false for objects with different number of keys", () => {
349
+ expect(deepEqual({ a: 1, b: 2 }, { a: 1 })).toBe(false)
350
+ expect(deepEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false)
351
+ })
352
+
353
+ it("returns true if object keys match but values are objects of different types", () => {
354
+ expect(deepEqual({ a: [1, 2] }, { a: { 0: 1, 1: 2 } })).toBe(true)
355
+ })
356
+
357
+ it("returns false for objects with missing keys", () => {
358
+ expect(deepEqual({ a: 1, b: 2 }, { a: 1 })).toBe(false)
359
+ })
360
+
361
+ it("returns true for objects with undefined values if both have them", () => {
362
+ expect(deepEqual({ a: undefined }, { a: undefined })).toBe(true)
363
+ })
364
+
365
+ it("returns false if one object has undefined key and the other doesn't", () => {
366
+ expect(deepEqual({ a: undefined }, {})).toBe(false)
367
+ expect(deepEqual({}, { a: undefined })).toBe(false)
368
+ })
369
+ })