@tim-code/my-util 0.0.23 → 0.1.0

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,10 +1,11 @@
1
1
  {
2
2
  "name": "@tim-code/my-util",
3
- "version": "0.0.23",
3
+ "version": "0.1.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "",
7
7
  "license": "MIT",
8
+ "sideEffects": false,
8
9
  "main": "src/index.js",
9
10
  "exports": {
10
11
  ".": "./src/index.js",
@@ -21,9 +22,8 @@
21
22
  ]
22
23
  },
23
24
  "devDependencies": {
24
- "@tim-code/eslint-config": "^1.1.7",
25
+ "@tim-code/eslint-config": "^1.3.3",
25
26
  "@jest/globals": "^29.7.0",
26
- "@types/node": ">=20 <21",
27
27
  "jest": "^29.7.0"
28
28
  },
29
29
  "jest": {
package/src/array.js CHANGED
@@ -41,6 +41,15 @@ export function mutateValues(object, callback) {
41
41
  return object
42
42
  }
43
43
 
44
+ /**
45
+ * Creates a function that accesses an object's value at key.
46
+ * @param {string} key
47
+ * @returns {any}
48
+ */
49
+ export function via(key) {
50
+ return (object) => object[key]
51
+ }
52
+
44
53
  // sorts undefined and null to the end if applicable
45
54
  function compareUndefinedNull(a, b) {
46
55
  if (b === undefined || b === null) {
@@ -109,30 +118,139 @@ export function multilevel(...comparators) {
109
118
  }
110
119
  }
111
120
 
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]
121
+ function findClosestAbs(array, value, { key, threshold = Infinity } = {}) {
122
+ let closest
123
+ if (key) {
124
+ for (const element of array) {
125
+ const _value = element[key]
126
+ const diff = Math.abs(_value - value)
127
+ if (diff < threshold) {
128
+ closest = element
129
+ threshold = diff
130
+ }
131
+ }
132
+ } else {
133
+ for (const _value of array) {
134
+ const diff = Math.abs(_value - value)
135
+ if (diff < threshold) {
136
+ closest = _value
137
+ threshold = diff
138
+ }
139
+ }
140
+ }
141
+ return closest
142
+ }
143
+
144
+ function findClosestLT(array, value, { key, threshold = -Infinity } = {}) {
145
+ let closest
146
+ if (key) {
147
+ for (const element of array) {
148
+ const _value = element[key]
149
+ if (_value < value && _value > threshold) {
150
+ closest = element
151
+ threshold = _value
152
+ }
153
+ }
154
+ } else {
155
+ for (const _value of array) {
156
+ if (_value < value && _value > threshold) {
157
+ closest = _value
158
+ threshold = _value
159
+ }
160
+ }
161
+ }
162
+ return closest
163
+ }
164
+
165
+ function findClosestLTE(array, value, { key, threshold = -Infinity } = {}) {
166
+ let closest
167
+ if (key) {
168
+ for (const element of array) {
169
+ const _value = element[key]
170
+ if (_value <= value && _value > threshold) {
171
+ closest = element
172
+ threshold = _value
173
+ }
174
+ }
175
+ } else {
176
+ for (const _value of array) {
177
+ if (_value <= value && _value > threshold) {
178
+ closest = _value
179
+ threshold = _value
180
+ }
181
+ }
182
+ }
183
+ return closest
184
+ }
185
+
186
+ function findClosestGT(array, value, { key, threshold = Infinity } = {}) {
187
+ let closest
188
+ if (key) {
189
+ for (const element of array) {
190
+ const _value = element[key]
191
+ if (_value > value && _value < threshold) {
192
+ closest = element
193
+ threshold = _value
194
+ }
195
+ }
196
+ } else {
197
+ for (const _value of array) {
198
+ if (_value > value && _value < threshold) {
199
+ closest = _value
200
+ threshold = _value
201
+ }
202
+ }
203
+ }
204
+ return closest
119
205
  }
120
206
 
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
- // }
207
+ function findClosestGTE(array, value, { key, threshold = Infinity } = {}) {
208
+ let closest
209
+ if (key) {
210
+ for (const element of array) {
211
+ const _value = element[key]
212
+ if (_value >= value && _value < threshold) {
213
+ closest = element
214
+ threshold = _value
215
+ }
216
+ }
217
+ } else {
218
+ for (const _value of array) {
219
+ if (_value >= value && _value < threshold) {
220
+ closest = _value
221
+ threshold = _value
222
+ }
223
+ }
224
+ }
225
+ return closest
226
+ }
131
227
 
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
- // }
228
+ /**
229
+ * Find the closest element in an array.
230
+ * If using for strings, need to specify different values for "threshold" and "comparator".
231
+ * "~" and "" are good threshold string values for gt/gte and lt/lte respectively.
232
+ * @param {Array<T>} array
233
+ * @param {T} value
234
+ * @param {Object} options
235
+ * @param {string=} options.key If specified, will consider the value for each element's key instead of the element itself.
236
+ * @param {string=} options.comparator "abs", "lt", "lte", "gt", "gte", "abs". Default is "abs" which implies T is number.
237
+ * @param {T=} options.threshold If specified, uses a different initial min/max/difference than positive or negative infinity.
238
+ * @returns {T|undefined}
239
+ */
240
+ export function findClosest(array, value, options = {}) {
241
+ const { comparator = "abs" } = options
242
+ switch (comparator) {
243
+ case "lt":
244
+ return findClosestLT(array, value, options)
245
+ case "lte":
246
+ return findClosestLTE(array, value, options)
247
+ case "gt":
248
+ return findClosestGT(array, value, options)
249
+ case "gte":
250
+ return findClosestGTE(array, value, options)
251
+ case "abs":
252
+ return findClosestAbs(array, value, options)
253
+ default:
254
+ throw new Error(`Unknown comparator: ${comparator}`)
255
+ }
256
+ }
package/src/array.test.js CHANGED
@@ -1,7 +1,10 @@
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, mutateValues, ascending, descending, multilevel, via } = await import(
6
+ "./array.js"
7
+ )
5
8
 
6
9
  describe("chunk", () => {
7
10
  it("splits array into chunks of specified size", () => {
@@ -231,9 +234,18 @@ describe("multilevel", () => {
231
234
 
232
235
  it("short-circuits after first non-zero comparator", () => {
233
236
  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 })
237
+ const cmp1 = jest.fn(() => {
238
+ calls.push("cmp1")
239
+ return 0
240
+ })
241
+ const cmp2 = jest.fn(() => {
242
+ calls.push("cmp2")
243
+ return -1
244
+ })
245
+ const cmp3 = jest.fn(() => {
246
+ calls.push("cmp3")
247
+ return 1
248
+ })
237
249
  const cmp = multilevel(cmp1, cmp2, cmp3)
238
250
  expect(cmp({}, {})).toBe(-1)
239
251
  expect(calls).toEqual(["cmp1", "cmp2"])
@@ -271,3 +283,153 @@ describe("via", () => {
271
283
  expect(() => getFoo(null)).toThrow(TypeError)
272
284
  })
273
285
  })
286
+
287
+ describe("findClosest", () => {
288
+ it("returns the closest value by absolute difference (default comparator)", () => {
289
+ expect(findClosest([1, 5, 10], 6)).toBe(5)
290
+ expect(findClosest([1, 5, 10], 8)).toBe(10)
291
+ expect(findClosest([1, 5, 10], 1)).toBe(1)
292
+ })
293
+
294
+ it("returns undefined if array is empty", () => {
295
+ expect(findClosest([], 10)).toBeUndefined()
296
+ })
297
+
298
+ it("returns the closest object by key (abs)", () => {
299
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
300
+ expect(findClosest(arr, 6, { key: "v" })).toEqual({ v: 5 })
301
+ })
302
+
303
+ it("returns the closest value less than input (lt comparator)", () => {
304
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "lt" })).toBe(5)
305
+ expect(findClosest([1, 3, 5, 7], 1, { comparator: "lt" })).toBeUndefined()
306
+ })
307
+
308
+ it("returns the closest value less than or equal to input (lte comparator)", () => {
309
+ expect(findClosest([1, 3, 5, 7], 5, { comparator: "lte" })).toBe(5)
310
+ expect(findClosest([1, 3, 5, 7], 2, { comparator: "lte" })).toBe(1)
311
+ expect(findClosest([1, 3, 5, 7], 0, { comparator: "lte" })).toBeUndefined()
312
+ })
313
+
314
+ it("returns the closest value greater than input (gt comparator)", () => {
315
+ expect(findClosest([1, 3, 5, 7], 5, { comparator: "gt" })).toBe(7)
316
+ expect(findClosest([1, 3, 5, 7], 7, { comparator: "gt" })).toBeUndefined()
317
+ })
318
+
319
+ it("returns the closest value greater than or equal to input (gte comparator)", () => {
320
+ expect(findClosest([1, 3, 5, 7], 5, { comparator: "gte" })).toBe(5)
321
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "gte" })).toBe(7)
322
+ expect(findClosest([1, 3, 5, 7], 8, { comparator: "gte" })).toBeUndefined()
323
+ })
324
+
325
+ it("returns the closest object by key for lt/lte/gt/gte", () => {
326
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
327
+ expect(findClosest(arr, 6, { comparator: "lt", key: "v" })).toEqual({ v: 5 })
328
+ expect(findClosest(arr, 6, { comparator: "lte", key: "v" })).toEqual({ v: 5 })
329
+ expect(findClosest(arr, 6, { comparator: "gt", key: "v" })).toEqual({ v: 10 })
330
+ expect(findClosest(arr, 10, { comparator: "gte", key: "v" })).toEqual({ v: 10 })
331
+ })
332
+
333
+ it("respects the threshold option for abs comparator", () => {
334
+ expect(findClosest([1, 5, 10], 6, { threshold: 0.5 })).toBeUndefined()
335
+ expect(findClosest([1, 5, 10], 6, { threshold: 2 })).toBe(5)
336
+ })
337
+
338
+ it("respects the threshold option for lt/lte/gt/gte", () => {
339
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "lt", threshold: 4 })).toBe(5)
340
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "lt", threshold: 5 })).toBeUndefined()
341
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "gt", threshold: 7 })).toBeUndefined()
342
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "gt", threshold: 10 })).toBe(7)
343
+ })
344
+
345
+ it("throws for unknown comparator", () => {
346
+ expect(() => findClosest([1, 2, 3], 2, { comparator: "foo" })).toThrow(
347
+ "Unknown comparator: foo"
348
+ )
349
+ })
350
+
351
+ it("returns undefined if no element matches threshold/key criteria", () => {
352
+ expect(
353
+ findClosest([{ v: 1 }], 10, { comparator: "gt", key: "v", threshold: 1 })
354
+ ).toBeUndefined()
355
+ expect(
356
+ findClosest([{ v: 1 }], 0, { comparator: "lt", key: "v", threshold: 1 })
357
+ ).toBeUndefined()
358
+ })
359
+
360
+ it("works with negative numbers and zero", () => {
361
+ expect(findClosest([-10, -5, 0, 5, 10], -7)).toBe(-5)
362
+ expect(findClosest([-10, -5, 0, 5, 10], 0)).toBe(0)
363
+ expect(findClosest([-10, -5, 0, 5, 10], 7)).toBe(5)
364
+ })
365
+
366
+ it("skips NaN values in abs comparator", () => {
367
+ expect(findClosest([1, NaN, 5], 4)).toBe(5)
368
+ })
369
+
370
+ it("skips objects missing the key in key-based comparators", () => {
371
+ const arr = [{ v: 1 }, {}, { v: 5 }]
372
+ expect(findClosest(arr, 2, { key: "v" })).toEqual({ v: 1 })
373
+ })
374
+
375
+ it("finds the closest string using abs comparator and a custom threshold/comparator", () => {
376
+ // Since abs comparator expects numbers, we need to provide a custom comparator for strings.
377
+ // We'll use threshold and comparator: "lt", "lte", "gt", "gte" for string comparisons.
378
+ const arr = ["apple", "banana", "cherry", "date"]
379
+ // Find the closest string less than "carrot" (alphabetically)
380
+ expect(findClosest(arr, "carrot", { comparator: "lt", threshold: "" })).toBe("banana")
381
+ // Find the closest string less than or equal to "banana"
382
+ expect(findClosest(arr, "banana", { comparator: "lte", threshold: "" })).toBe("banana")
383
+ // Find the closest string greater than "carrot"
384
+ expect(findClosest(arr, "carrot", { comparator: "gt", threshold: "~" })).toBe("cherry")
385
+ // Find the closest string greater than or equal to "date"
386
+ expect(findClosest(arr, "date", { comparator: "gte", threshold: "~" })).toBe("date")
387
+ // If nothing matches, returns undefined
388
+ expect(findClosest(arr, "aardvark", { comparator: "lt", threshold: "" })).toBeUndefined()
389
+ expect(findClosest(arr, "zebra", { comparator: "gt", threshold: "~" })).toBeUndefined()
390
+ })
391
+
392
+ it("finds the closest string by key in array of objects", () => {
393
+ const arr = [{ name: "apple" }, { name: "banana" }, { name: "cherry" }]
394
+ expect(
395
+ findClosest(arr, "blueberry", { comparator: "lt", key: "name", threshold: "" })
396
+ ).toEqual({
397
+ name: "banana",
398
+ })
399
+ expect(
400
+ findClosest(arr, "banana", { comparator: "lte", key: "name", threshold: "" })
401
+ ).toEqual({
402
+ name: "banana",
403
+ })
404
+ expect(
405
+ findClosest(arr, "banana", { comparator: "gt", key: "name", threshold: "~" })
406
+ ).toEqual({
407
+ name: "cherry",
408
+ })
409
+ expect(
410
+ findClosest(arr, "cherry", { comparator: "gte", key: "name", threshold: "~" })
411
+ ).toEqual({
412
+ name: "cherry",
413
+ })
414
+ expect(
415
+ findClosest(arr, "aardvark", { comparator: "lt", key: "name", threshold: "" })
416
+ ).toBeUndefined()
417
+ })
418
+
419
+ it("returns undefined if no string matches threshold/key criteria", () => {
420
+ const arr = ["apple", "banana", "cherry"]
421
+ expect(findClosest(arr, "apple", { comparator: "lt", threshold: "" })).toBeUndefined()
422
+ expect(findClosest(arr, "cherry", { comparator: "gt" })).toBeUndefined()
423
+ })
424
+
425
+ it("can use abs comparator with string lengths", () => {
426
+ // This is a reasonable use-case for abs: find string with length closest to 4
427
+ const arr = ["a", "bb", "ccc", "dddd", "eeeee"]
428
+ // Map to string lengths using key
429
+ expect(findClosest(arr, 4, { comparator: "abs", key: "length" })).toEqual("dddd")
430
+ // If threshold is set so no string length is close enough
431
+ expect(
432
+ findClosest(arr, 4, { comparator: "abs", key: "length", threshold: -1 })
433
+ ).toBeUndefined()
434
+ })
435
+ })