@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 +3 -3
- package/src/array.js +142 -24
- package/src/array.test.js +166 -4
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tim-code/my-util",
|
|
3
|
-
"version": "0.0
|
|
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.
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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(
|
|
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(() => {
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
})
|