@tim-code/my-util 0.0.24 → 0.1.1

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/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  This library includes common util functions. It does not have any dependencies.
4
4
 
5
- This may include rewritten functionality from Lodash in the future (i.e. orderBy). groupBy should be available when using Node 22.
5
+ In combination with Object.groupBy (available when using Node 22), this should be sufficient for replacing the most useful functionality from lodash.
6
6
 
7
7
  ## First Time Setup
8
8
 
9
- `npm install`
9
+ `npm install @tim-code/my-util`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tim-code/my-util",
3
- "version": "0.0.24",
3
+ "version": "0.1.1",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "",
@@ -22,7 +22,7 @@
22
22
  ]
23
23
  },
24
24
  "devDependencies": {
25
- "@tim-code/eslint-config": "^1.1.7",
25
+ "@tim-code/eslint-config": "^1.3.3",
26
26
  "@jest/globals": "^29.7.0",
27
27
  "jest": "^29.7.0"
28
28
  },
package/src/array.js CHANGED
@@ -28,19 +28,6 @@ export function unique(array) {
28
28
  return [...new Set(array)]
29
29
  }
30
30
 
31
- /**
32
- * Mutates the passed in object by calling callback on each of its values.
33
- * @param {Object} object
34
- * @param {Function} callback (value, key, object) => newValue // note if not changing value, should return value
35
- * @returns {Object}
36
- */
37
- export function mutateValues(object, callback) {
38
- for (const key in object) {
39
- object[key] = callback(object[key], key, object)
40
- }
41
- return object
42
- }
43
-
44
31
  // sorts undefined and null to the end if applicable
45
32
  function compareUndefinedNull(a, b) {
46
33
  if (b === undefined || b === null) {
@@ -109,30 +96,139 @@ export function multilevel(...comparators) {
109
96
  }
110
97
  }
111
98
 
112
- /**
113
- * Creates a function that accesses an object's value at key.
114
- * @param {string} key
115
- * @returns {any}
116
- */
117
- export function via(key) {
118
- return (object) => object[key]
99
+ function findClosestAbs(array, value, { key, threshold = Infinity } = {}) {
100
+ let closest
101
+ if (key) {
102
+ for (const element of array) {
103
+ const _value = element[key]
104
+ const diff = Math.abs(_value - value)
105
+ if (diff < threshold) {
106
+ closest = element
107
+ threshold = diff
108
+ }
109
+ }
110
+ } else {
111
+ for (const _value of array) {
112
+ const diff = Math.abs(_value - value)
113
+ if (diff < threshold) {
114
+ closest = _value
115
+ threshold = diff
116
+ }
117
+ }
118
+ }
119
+ return closest
119
120
  }
120
121
 
121
- // export function contains(template) {
122
- // return (object) => {
123
- // for (const key in template) {
124
- // if (object[key] !== template[key]) {
125
- // return false
126
- // }
127
- // }
128
- // return true
129
- // }
130
- // }
122
+ function findClosestLT(array, value, { key, threshold = -Infinity } = {}) {
123
+ let closest
124
+ if (key) {
125
+ for (const element of array) {
126
+ const _value = element[key]
127
+ if (_value < value && _value > threshold) {
128
+ closest = element
129
+ threshold = _value
130
+ }
131
+ }
132
+ } else {
133
+ for (const _value of array) {
134
+ if (_value < value && _value > threshold) {
135
+ closest = _value
136
+ threshold = _value
137
+ }
138
+ }
139
+ }
140
+ return closest
141
+ }
131
142
 
132
- // not sure how far we want to go down "key" rabbit hole:
133
- // export function sum(array, key) {}
134
- // or maybe
135
- // export function add(key) {
136
- // if(!key) return (acc, value) => acc + value
137
- // return (acc, obj) => acc + obj[key]
138
- // }
143
+ function findClosestLTE(array, value, { key, threshold = -Infinity } = {}) {
144
+ let closest
145
+ if (key) {
146
+ for (const element of array) {
147
+ const _value = element[key]
148
+ if (_value <= value && _value > threshold) {
149
+ closest = element
150
+ threshold = _value
151
+ }
152
+ }
153
+ } else {
154
+ for (const _value of array) {
155
+ if (_value <= value && _value > threshold) {
156
+ closest = _value
157
+ threshold = _value
158
+ }
159
+ }
160
+ }
161
+ return closest
162
+ }
163
+
164
+ function findClosestGT(array, value, { key, threshold = Infinity } = {}) {
165
+ let closest
166
+ if (key) {
167
+ for (const element of array) {
168
+ const _value = element[key]
169
+ if (_value > value && _value < threshold) {
170
+ closest = element
171
+ threshold = _value
172
+ }
173
+ }
174
+ } else {
175
+ for (const _value of array) {
176
+ if (_value > value && _value < threshold) {
177
+ closest = _value
178
+ threshold = _value
179
+ }
180
+ }
181
+ }
182
+ return closest
183
+ }
184
+
185
+ function findClosestGTE(array, value, { key, threshold = Infinity } = {}) {
186
+ let closest
187
+ if (key) {
188
+ for (const element of array) {
189
+ const _value = element[key]
190
+ if (_value >= value && _value < threshold) {
191
+ closest = element
192
+ threshold = _value
193
+ }
194
+ }
195
+ } else {
196
+ for (const _value of array) {
197
+ if (_value >= value && _value < threshold) {
198
+ closest = _value
199
+ threshold = _value
200
+ }
201
+ }
202
+ }
203
+ return closest
204
+ }
205
+
206
+ /**
207
+ * Find the closest element in an array.
208
+ * If using for strings, need to specify different values for "threshold" and "comparator".
209
+ * "~" and "" are good threshold string values for gt/gte and lt/lte respectively.
210
+ * @param {Array<T>} array
211
+ * @param {T} value
212
+ * @param {Object} options
213
+ * @param {string=} options.key If specified, will consider the value for each element's key instead of the element itself.
214
+ * @param {string=} options.comparator "abs", "lt", "lte", "gt", "gte", "abs". Default is "abs" which implies T is number.
215
+ * @param {T=} options.threshold If specified, uses a different initial min/max/difference than positive or negative infinity.
216
+ * @returns {T|undefined}
217
+ */
218
+ export function findClosest(array, value, options = {}) {
219
+ const { comparator = "abs" } = options
220
+ switch (comparator) {
221
+ case "lt":
222
+ return findClosestLT(array, value, options)
223
+ case "lte":
224
+ return findClosestLTE(array, value, options)
225
+ case "gt":
226
+ return findClosestGT(array, value, options)
227
+ case "gte":
228
+ return findClosestGTE(array, value, options)
229
+ case "abs":
230
+ return findClosestAbs(array, value, options)
231
+ default:
232
+ throw new Error(`Unknown comparator: ${comparator}`)
233
+ }
234
+ }
package/src/array.test.js CHANGED
@@ -1,7 +1,8 @@
1
1
  /* eslint-disable no-restricted-syntax */
2
2
  import { describe, expect, it, jest } from "@jest/globals"
3
+ import { findClosest } from "./array.js"
3
4
 
4
- const { chunk, unique, mutateValues, ascending, descending, multilevel, via } = await import("./array.js")
5
+ const { chunk, unique, ascending, descending, multilevel } = await import("./array.js")
5
6
 
6
7
  describe("chunk", () => {
7
8
  it("splits array into chunks of specified size", () => {
@@ -70,42 +71,6 @@ describe("unique", () => {
70
71
  })
71
72
  })
72
73
 
73
- describe("mutateValues", () => {
74
- it("mutates values in the object using the callback", () => {
75
- const obj = { a: 1, b: 2 }
76
- const result = mutateValues(obj, (v) => v * 2)
77
- expect(result).toEqual({ a: 2, b: 4 })
78
- expect(obj).toBe(result) // should mutate in place
79
- })
80
-
81
- it("callback receives value, key, and object", () => {
82
- const obj = { x: 1 }
83
- const cb = jest.fn((v) => v + 1)
84
- mutateValues(obj, cb)
85
- expect(cb).toHaveBeenCalledWith(1, "x", obj)
86
- })
87
-
88
- it("returns the same object reference", () => {
89
- const obj = { foo: "bar" }
90
- const returned = mutateValues(obj, (v) => v)
91
- expect(returned).toBe(obj)
92
- })
93
-
94
- it("handles empty object", () => {
95
- const obj = {}
96
- expect(mutateValues(obj, (v) => v)).toEqual({})
97
- })
98
-
99
- it("mutates inherited enumerable properties", () => {
100
- const proto = { inherited: 1 }
101
- const obj = Object.create(proto)
102
- obj.own = 2
103
- const result = mutateValues(obj, (v) => v + 1)
104
- expect(result.own).toBe(3)
105
- expect(result.inherited).toBe(2)
106
- })
107
- })
108
-
109
74
  describe("ascending", () => {
110
75
  it("sorts primitives ascending, undefined/null at end", () => {
111
76
  const arr = [undefined, null, 3, 1, 2]
@@ -231,9 +196,18 @@ describe("multilevel", () => {
231
196
 
232
197
  it("short-circuits after first non-zero comparator", () => {
233
198
  const calls = []
234
- const cmp1 = jest.fn(() => { calls.push("cmp1"); return 0 })
235
- const cmp2 = jest.fn(() => { calls.push("cmp2"); return -1 })
236
- const cmp3 = jest.fn(() => { calls.push("cmp3"); return 1 })
199
+ const cmp1 = jest.fn(() => {
200
+ calls.push("cmp1")
201
+ return 0
202
+ })
203
+ const cmp2 = jest.fn(() => {
204
+ calls.push("cmp2")
205
+ return -1
206
+ })
207
+ const cmp3 = jest.fn(() => {
208
+ calls.push("cmp3")
209
+ return 1
210
+ })
237
211
  const cmp = multilevel(cmp1, cmp2, cmp3)
238
212
  expect(cmp({}, {})).toBe(-1)
239
213
  expect(calls).toEqual(["cmp1", "cmp2"])
@@ -246,28 +220,152 @@ describe("multilevel", () => {
246
220
  })
247
221
  })
248
222
 
249
- describe("via", () => {
250
- it("returns a function that accesses the given key", () => {
251
- const getFoo = via("foo")
252
- expect(getFoo({ foo: 42 })).toBe(42)
253
- expect(getFoo({ foo: "bar" })).toBe("bar")
223
+ describe("findClosest", () => {
224
+ it("returns the closest value by absolute difference (default comparator)", () => {
225
+ expect(findClosest([1, 5, 10], 6)).toBe(5)
226
+ expect(findClosest([1, 5, 10], 8)).toBe(10)
227
+ expect(findClosest([1, 5, 10], 1)).toBe(1)
228
+ })
229
+
230
+ it("returns undefined if array is empty", () => {
231
+ expect(findClosest([], 10)).toBeUndefined()
232
+ })
233
+
234
+ it("returns the closest object by key (abs)", () => {
235
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
236
+ expect(findClosest(arr, 6, { key: "v" })).toEqual({ v: 5 })
237
+ })
238
+
239
+ it("returns the closest value less than input (lt comparator)", () => {
240
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "lt" })).toBe(5)
241
+ expect(findClosest([1, 3, 5, 7], 1, { comparator: "lt" })).toBeUndefined()
242
+ })
243
+
244
+ it("returns the closest value less than or equal to input (lte comparator)", () => {
245
+ expect(findClosest([1, 3, 5, 7], 5, { comparator: "lte" })).toBe(5)
246
+ expect(findClosest([1, 3, 5, 7], 2, { comparator: "lte" })).toBe(1)
247
+ expect(findClosest([1, 3, 5, 7], 0, { comparator: "lte" })).toBeUndefined()
248
+ })
249
+
250
+ it("returns the closest value greater than input (gt comparator)", () => {
251
+ expect(findClosest([1, 3, 5, 7], 5, { comparator: "gt" })).toBe(7)
252
+ expect(findClosest([1, 3, 5, 7], 7, { comparator: "gt" })).toBeUndefined()
254
253
  })
255
254
 
256
- it("returns undefined if the key does not exist", () => {
257
- const getX = via("x")
258
- expect(getX({})).toBeUndefined()
259
- expect(getX({ y: 1 })).toBeUndefined()
255
+ it("returns the closest value greater than or equal to input (gte comparator)", () => {
256
+ expect(findClosest([1, 3, 5, 7], 5, { comparator: "gte" })).toBe(5)
257
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "gte" })).toBe(7)
258
+ expect(findClosest([1, 3, 5, 7], 8, { comparator: "gte" })).toBeUndefined()
260
259
  })
261
260
 
262
- it("works with numeric keys", () => {
263
- const get0 = via(0)
264
- expect(get0([10, 20])).toBe(10)
265
- expect(get0({ 0: "zero" })).toBe("zero")
261
+ it("returns the closest object by key for lt/lte/gt/gte", () => {
262
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
263
+ expect(findClosest(arr, 6, { comparator: "lt", key: "v" })).toEqual({ v: 5 })
264
+ expect(findClosest(arr, 6, { comparator: "lte", key: "v" })).toEqual({ v: 5 })
265
+ expect(findClosest(arr, 6, { comparator: "gt", key: "v" })).toEqual({ v: 10 })
266
+ expect(findClosest(arr, 10, { comparator: "gte", key: "v" })).toEqual({ v: 10 })
267
+ })
268
+
269
+ it("respects the threshold option for abs comparator", () => {
270
+ expect(findClosest([1, 5, 10], 6, { threshold: 0.5 })).toBeUndefined()
271
+ expect(findClosest([1, 5, 10], 6, { threshold: 2 })).toBe(5)
272
+ })
273
+
274
+ it("respects the threshold option for lt/lte/gt/gte", () => {
275
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "lt", threshold: 4 })).toBe(5)
276
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "lt", threshold: 5 })).toBeUndefined()
277
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "gt", threshold: 7 })).toBeUndefined()
278
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "gt", threshold: 10 })).toBe(7)
279
+ })
280
+
281
+ it("throws for unknown comparator", () => {
282
+ expect(() => findClosest([1, 2, 3], 2, { comparator: "foo" })).toThrow(
283
+ "Unknown comparator: foo"
284
+ )
266
285
  })
267
286
 
268
- it("returns undefined if object is missing", () => {
269
- const getFoo = via("foo")
270
- expect(() => getFoo(undefined)).toThrow(TypeError)
271
- expect(() => getFoo(null)).toThrow(TypeError)
287
+ it("returns undefined if no element matches threshold/key criteria", () => {
288
+ expect(
289
+ findClosest([{ v: 1 }], 10, { comparator: "gt", key: "v", threshold: 1 })
290
+ ).toBeUndefined()
291
+ expect(
292
+ findClosest([{ v: 1 }], 0, { comparator: "lt", key: "v", threshold: 1 })
293
+ ).toBeUndefined()
294
+ })
295
+
296
+ it("works with negative numbers and zero", () => {
297
+ expect(findClosest([-10, -5, 0, 5, 10], -7)).toBe(-5)
298
+ expect(findClosest([-10, -5, 0, 5, 10], 0)).toBe(0)
299
+ expect(findClosest([-10, -5, 0, 5, 10], 7)).toBe(5)
300
+ })
301
+
302
+ it("skips NaN values in abs comparator", () => {
303
+ expect(findClosest([1, NaN, 5], 4)).toBe(5)
304
+ })
305
+
306
+ it("skips objects missing the key in key-based comparators", () => {
307
+ const arr = [{ v: 1 }, {}, { v: 5 }]
308
+ expect(findClosest(arr, 2, { key: "v" })).toEqual({ v: 1 })
309
+ })
310
+
311
+ it("finds the closest string using abs comparator and a custom threshold/comparator", () => {
312
+ // Since abs comparator expects numbers, we need to provide a custom comparator for strings.
313
+ // We'll use threshold and comparator: "lt", "lte", "gt", "gte" for string comparisons.
314
+ const arr = ["apple", "banana", "cherry", "date"]
315
+ // Find the closest string less than "carrot" (alphabetically)
316
+ expect(findClosest(arr, "carrot", { comparator: "lt", threshold: "" })).toBe("banana")
317
+ // Find the closest string less than or equal to "banana"
318
+ expect(findClosest(arr, "banana", { comparator: "lte", threshold: "" })).toBe("banana")
319
+ // Find the closest string greater than "carrot"
320
+ expect(findClosest(arr, "carrot", { comparator: "gt", threshold: "~" })).toBe("cherry")
321
+ // Find the closest string greater than or equal to "date"
322
+ expect(findClosest(arr, "date", { comparator: "gte", threshold: "~" })).toBe("date")
323
+ // If nothing matches, returns undefined
324
+ expect(findClosest(arr, "aardvark", { comparator: "lt", threshold: "" })).toBeUndefined()
325
+ expect(findClosest(arr, "zebra", { comparator: "gt", threshold: "~" })).toBeUndefined()
326
+ })
327
+
328
+ it("finds the closest string by key in array of objects", () => {
329
+ const arr = [{ name: "apple" }, { name: "banana" }, { name: "cherry" }]
330
+ expect(
331
+ findClosest(arr, "blueberry", { comparator: "lt", key: "name", threshold: "" })
332
+ ).toEqual({
333
+ name: "banana",
334
+ })
335
+ expect(
336
+ findClosest(arr, "banana", { comparator: "lte", key: "name", threshold: "" })
337
+ ).toEqual({
338
+ name: "banana",
339
+ })
340
+ expect(
341
+ findClosest(arr, "banana", { comparator: "gt", key: "name", threshold: "~" })
342
+ ).toEqual({
343
+ name: "cherry",
344
+ })
345
+ expect(
346
+ findClosest(arr, "cherry", { comparator: "gte", key: "name", threshold: "~" })
347
+ ).toEqual({
348
+ name: "cherry",
349
+ })
350
+ expect(
351
+ findClosest(arr, "aardvark", { comparator: "lt", key: "name", threshold: "" })
352
+ ).toBeUndefined()
353
+ })
354
+
355
+ it("returns undefined if no string matches threshold/key criteria", () => {
356
+ const arr = ["apple", "banana", "cherry"]
357
+ expect(findClosest(arr, "apple", { comparator: "lt", threshold: "" })).toBeUndefined()
358
+ expect(findClosest(arr, "cherry", { comparator: "gt" })).toBeUndefined()
359
+ })
360
+
361
+ it("can use abs comparator with string lengths", () => {
362
+ // This is a reasonable use-case for abs: find string with length closest to 4
363
+ const arr = ["a", "bb", "ccc", "dddd", "eeeee"]
364
+ // Map to string lengths using key
365
+ expect(findClosest(arr, 4, { comparator: "abs", key: "length" })).toEqual("dddd")
366
+ // If threshold is set so no string length is close enough
367
+ expect(
368
+ findClosest(arr, 4, { comparator: "abs", key: "length", threshold: -1 })
369
+ ).toBeUndefined()
272
370
  })
273
371
  })
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./array.js"
2
2
  export * from "./math.js"
3
+ export * from "./object.js"
3
4
  export * from "./promise.js"
4
5
  export * from "./time.js"
package/src/math.js CHANGED
@@ -9,6 +9,20 @@ export function mod(number, modulus) {
9
9
  return ((number % modulus) + modulus) % modulus
10
10
  }
11
11
 
12
+ /**
13
+ * Given two points, returns a function
14
+ * This function, given an "x" value, returns a "y" value that is on the same line as the first two points.
15
+ * @param {[number, number]} firstPoint
16
+ * @param {[number, number]} secondPoint
17
+ * @returns {Function}
18
+ */
19
+ export function line([x1, y1], [x2, y2]) {
20
+ const m = (y2 - y1) / (x2 - x1)
21
+ // m * x1 + b = y1
22
+ const b = y1 - m * x1
23
+ return (x) => m * x + b
24
+ }
25
+
12
26
  /**
13
27
  * Prepend a plus to a number or string if positive.
14
28
  * @param {number|string} number Or string
package/src/math.test.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "@jest/globals"
2
- const { mod, formatPlus } = await import("./math.js")
2
+ const { mod, formatPlus, line } = await import("./math.js")
3
3
 
4
4
  describe("mod", () => {
5
5
  it("returns n when n is less than m and n is non-negative", () => {
@@ -58,6 +58,52 @@ describe("mod", () => {
58
58
  })
59
59
  })
60
60
 
61
+ describe("line", () => {
62
+ it("returns a function that produces y for given x on the line through two points", () => {
63
+ const f = line([0, 0], [1, 1])
64
+ expect(f(0)).toBe(0)
65
+ expect(f(1)).toBe(1)
66
+ expect(f(2)).toBe(2)
67
+ expect(f(-1)).toBe(-1)
68
+ })
69
+
70
+ it("handles non-origin, non-unit slope", () => {
71
+ const f = line([1, 2], [3, 6]) // slope 2, intercept 0
72
+ expect(f(1)).toBe(2)
73
+ expect(f(2)).toBe(4)
74
+ expect(f(3)).toBe(6)
75
+ expect(f(0)).toBe(0)
76
+ })
77
+
78
+ it("handles negative slope", () => {
79
+ const f = line([0, 0], [2, -2]) // slope -1
80
+ expect(f(1)).toBe(-1)
81
+ expect(f(2)).toBe(-2)
82
+ expect(f(-2)).toBe(2)
83
+ })
84
+
85
+ it("handles horizontal line", () => {
86
+ const f = line([1, 5], [3, 5]) // slope 0
87
+ expect(f(0)).toBe(5)
88
+ expect(f(100)).toBe(5)
89
+ expect(f(-100)).toBe(5)
90
+ })
91
+
92
+ it("handles vertical line by returning NaN", () => {
93
+ const f = line([2, 1], [2, 5])
94
+ expect(f(2)).toBeNaN()
95
+ expect(f(3)).toBeNaN()
96
+ expect(f(1)).toBeNaN()
97
+ })
98
+
99
+ it("works with negative coordinates", () => {
100
+ const f = line([-1, -2], [1, 2]) // slope 2
101
+ expect(f(-1)).toBe(-2)
102
+ expect(f(0)).toBe(0)
103
+ expect(f(1)).toBe(2)
104
+ })
105
+ })
106
+
61
107
  describe("formatPlus", () => {
62
108
  it("prepends a plus for positive numbers", () => {
63
109
  expect(formatPlus(1)).toBe("+1")
package/src/object.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Mutates the passed in object by calling callback on each of its values.
3
+ * @param {Object} object
4
+ * @param {Function} callback (value, key, object) => newValue // note if not changing value, should return value
5
+ * @returns {Object}
6
+ */
7
+ export function mutateValues(object, callback) {
8
+ const keys = Object.keys(object)
9
+ for (const key of keys) {
10
+ object[key] = callback(object[key], key, object)
11
+ }
12
+ return object
13
+ }
14
+
15
+ /**
16
+ * Mutates the passed object by removing any keys (`key in object`) that have a value of undefined.
17
+ * This is useful when the presence of a key alone causes something else to happen, but an undefined value is unexpected.
18
+ * In general, having objects with undefined values should not be encouraged but can happen as a byproduct of a code flow.
19
+ * @param {Object} object
20
+ * @returns {Object}
21
+ */
22
+ export function deleteUndefinedValues(object) {
23
+ const keys = Object.keys(object)
24
+ for (const key of keys) {
25
+ if (key in object && object[key] === undefined) {
26
+ delete object[key]
27
+ }
28
+ }
29
+ return object
30
+ }
31
+
32
+ /**
33
+ * Creates a function that accesses an object's value at key.
34
+ * @param {string} key
35
+ * @returns {Function}
36
+ */
37
+ export function via(key) {
38
+ return (object) => object[key]
39
+ }
40
+
41
+ /**
42
+ * Creates a function that checks if the passed object contains the initial template.
43
+ * This means for each key in the template, the passed object has the same (===) value.
44
+ * @param {Object} template
45
+ * @returns {Function}
46
+ */
47
+ export function contains(template) {
48
+ const keys = Object.keys(template)
49
+ return (object) => {
50
+ for (const key of keys) {
51
+ if (object[key] !== template[key]) {
52
+ return false
53
+ }
54
+ }
55
+ return true
56
+ }
57
+ }
@@ -0,0 +1,161 @@
1
+ /* eslint-disable no-restricted-syntax */
2
+ import { jest } from "@jest/globals"
3
+ import { contains, deleteUndefinedValues, mutateValues, via } from "./object.js"
4
+
5
+ describe("mutateValues", () => {
6
+ it("mutates values in the object using the callback", () => {
7
+ const obj = { a: 1, b: 2 }
8
+ const result = mutateValues(obj, (v) => v * 2)
9
+ expect(result).toEqual({ a: 2, b: 4 })
10
+ expect(obj).toBe(result) // should mutate in place
11
+ })
12
+
13
+ it("callback receives value, key, and object", () => {
14
+ const obj = { x: 1 }
15
+ const cb = jest.fn((v) => v + 1)
16
+ mutateValues(obj, cb)
17
+ expect(cb).toHaveBeenCalledWith(1, "x", obj)
18
+ })
19
+
20
+ it("returns the same object reference", () => {
21
+ const obj = { foo: "bar" }
22
+ const returned = mutateValues(obj, (v) => v)
23
+ expect(returned).toBe(obj)
24
+ })
25
+
26
+ it("handles empty object", () => {
27
+ const obj = {}
28
+ expect(mutateValues(obj, (v) => v)).toEqual({})
29
+ })
30
+
31
+ it("does not mutate inherited enumerable properties", () => {
32
+ const proto = { inherited: 1 }
33
+ const obj = Object.create(proto)
34
+ obj.own = 2
35
+ const result = mutateValues(obj, (v) => v + 1)
36
+ expect(result.own).toBe(3)
37
+ expect(result.inherited).toBe(1)
38
+ })
39
+ })
40
+
41
+ describe("deleteUndefinedValues", () => {
42
+ it("removes keys with undefined values", () => {
43
+ const obj = { a: 1, b: undefined, c: 3 }
44
+ const result = deleteUndefinedValues(obj)
45
+ expect(result).toEqual({ a: 1, c: 3 })
46
+ expect(obj).toBe(result)
47
+ expect("b" in result).toBe(false)
48
+ })
49
+
50
+ it("does not remove keys with null or falsy non-undefined values", () => {
51
+ const obj = { a: null, b: 0, c: false, d: "", e: undefined }
52
+ deleteUndefinedValues(obj)
53
+ expect(obj).toEqual({ a: null, b: 0, c: false, d: "" })
54
+ })
55
+
56
+ it("does nothing if no undefined values are present", () => {
57
+ const obj = { a: 1, b: 2 }
58
+ const result = deleteUndefinedValues(obj)
59
+ expect(result).toEqual({ a: 1, b: 2 })
60
+ })
61
+
62
+ it("handles empty object", () => {
63
+ const obj = {}
64
+ expect(deleteUndefinedValues(obj)).toEqual({})
65
+ })
66
+
67
+ it("removes own properties set to undefined but not inherited ones", () => {
68
+ const proto = { inherited: undefined }
69
+ const obj = Object.create(proto)
70
+ obj.own = undefined
71
+ deleteUndefinedValues(obj)
72
+ expect("own" in obj).toBe(false)
73
+ // inherited property remains accessible via prototype
74
+ expect(obj.inherited).toBeUndefined()
75
+ // but is not an own property
76
+ expect(Object.hasOwn(obj, "inherited")).toBe(false)
77
+ })
78
+ })
79
+
80
+ describe("via", () => {
81
+ it("returns a function that accesses the given key", () => {
82
+ const getFoo = via("foo")
83
+ expect(getFoo({ foo: 42 })).toBe(42)
84
+ expect(getFoo({ foo: "bar" })).toBe("bar")
85
+ })
86
+
87
+ it("returns undefined if the key does not exist", () => {
88
+ const getX = via("x")
89
+ expect(getX({})).toBeUndefined()
90
+ expect(getX({ y: 1 })).toBeUndefined()
91
+ })
92
+
93
+ it("works with numeric keys", () => {
94
+ const get0 = via(0)
95
+ expect(get0([10, 20])).toBe(10)
96
+ expect(get0({ 0: "zero" })).toBe("zero")
97
+ })
98
+
99
+ it("returns undefined if object is missing", () => {
100
+ const getFoo = via("foo")
101
+ expect(() => getFoo(undefined)).toThrow(TypeError)
102
+ expect(() => getFoo(null)).toThrow(TypeError)
103
+ })
104
+ })
105
+
106
+ describe("contains", () => {
107
+ it("returns true when object contains all template keys with same values", () => {
108
+ const template = { a: 1, b: 2 }
109
+ const fn = contains(template)
110
+ expect(fn({ a: 1, b: 2, c: 3 })).toBe(true)
111
+ expect(fn({ a: 1, b: 2 })).toBe(true)
112
+ })
113
+
114
+ it("returns false if any template key is missing", () => {
115
+ const template = { a: 1, b: 2 }
116
+ const fn = contains(template)
117
+ expect(fn({ a: 1 })).toBe(false)
118
+ expect(fn({ b: 2 })).toBe(false)
119
+ expect(fn({})).toBe(false)
120
+ })
121
+
122
+ it("returns false if any template key has a different value", () => {
123
+ const template = { a: 1, b: 2 }
124
+ const fn = contains(template)
125
+ expect(fn({ a: 1, b: 3 })).toBe(false)
126
+ expect(fn({ a: 2, b: 2 })).toBe(false)
127
+ expect(fn({ a: 2, b: 3 })).toBe(false)
128
+ })
129
+
130
+ it("works with empty template (always true)", () => {
131
+ const fn = contains({})
132
+ expect(fn({})).toBe(true)
133
+ expect(fn({ a: 1 })).toBe(true)
134
+ expect(fn({ a: undefined })).toBe(true)
135
+ })
136
+
137
+ it("does not require object to have only template keys", () => {
138
+ const template = { x: 5 }
139
+ const fn = contains(template)
140
+ expect(fn({ x: 5, y: 10 })).toBe(true)
141
+ })
142
+
143
+ it("uses strict equality (===) for comparison", () => {
144
+ const template = { a: 0 }
145
+ const fn = contains(template)
146
+ expect(fn({ a: false })).toBe(false)
147
+ expect(fn({ a: "0" })).toBe(false)
148
+ expect(fn({ a: 0 })).toBe(true)
149
+ })
150
+
151
+ // ISSUE: contains() does not check for own properties only; it will match inherited properties.
152
+ it("matches inherited properties in the object", () => {
153
+ const template = { foo: 1 }
154
+ const fn = contains(template)
155
+ const proto = { foo: 1 }
156
+ const obj = Object.create(proto)
157
+ expect(fn(obj)).toBe(true)
158
+ obj.foo = 2
159
+ expect(fn(obj)).toBe(false)
160
+ })
161
+ })