@tim-code/my-util 0.1.2 → 0.1.4

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.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "",
package/src/array.js CHANGED
@@ -44,38 +44,58 @@ function compareUndefinedNull(a, b) {
44
44
  /**
45
45
  * Returns an "ascending" comparator, via "<", to be used to sort an array.
46
46
  * Undefined or null values are always sorted to the end.
47
- * @param {String=} key If sorting objects, can specify a key to use to compare.
47
+ * @param {string|number|Function=} transform
48
+ * If a function, calls the provided function on an element to get the value to sort on.
49
+ * If a string or number, treats transform as a key and sorts on each element's value at key.
48
50
  * @returns {Function}
49
51
  */
50
- export function ascending(key) {
51
- if (!key) {
52
+ export function ascending(transform) {
53
+ if (typeof transform === "function") {
52
54
  return (a, b) => {
55
+ a = transform(a)
56
+ b = transform(b)
53
57
  return compareUndefinedNull(a, b) ?? (a < b ? -1 : b < a ? 1 : 0)
54
58
  }
55
59
  }
60
+ if (typeof transform === "string" || typeof transform === "number") {
61
+ return (a, b) => {
62
+ const invalid = compareUndefinedNull(a[transform], b[transform])
63
+ return (
64
+ invalid ?? (a[transform] < b[transform] ? -1 : b[transform] < a[transform] ? 1 : 0)
65
+ )
66
+ }
67
+ }
56
68
  return (a, b) => {
57
- return (
58
- compareUndefinedNull(a[key], b[key]) ?? (a[key] < b[key] ? -1 : b[key] < a[key] ? 1 : 0)
59
- )
69
+ return compareUndefinedNull(a, b) ?? (a < b ? -1 : b < a ? 1 : 0)
60
70
  }
61
71
  }
62
72
 
63
73
  /**
64
74
  * Returns a "descending" comparator, via ">", to be used to sort an array.
65
75
  * Undefined or null values are always sorted to the end.
66
- * @param {String=} key If sorting objects, can specify a key to use to compare.
76
+ * @param {string|number|Function=} transform
77
+ * If a function, calls the provided function on an element to get the value to sort on.
78
+ * If a string or number, treats transform as a key and sorts on each element's value at key.
67
79
  * @returns {Function}
68
80
  */
69
- export function descending(key) {
70
- if (!key) {
81
+ export function descending(transform) {
82
+ if (typeof transform === "function") {
71
83
  return (a, b) => {
84
+ a = transform(a)
85
+ b = transform(b)
72
86
  return compareUndefinedNull(a, b) ?? (a > b ? -1 : b > a ? 1 : 0)
73
87
  }
74
88
  }
89
+ if (typeof transform === "string" || typeof transform === "number") {
90
+ return (a, b) => {
91
+ const invalid = compareUndefinedNull(a[transform], b[transform])
92
+ return (
93
+ invalid ?? (a[transform] > b[transform] ? -1 : b[transform] > a[transform] ? 1 : 0)
94
+ )
95
+ }
96
+ }
75
97
  return (a, b) => {
76
- return (
77
- compareUndefinedNull(a[key], b[key]) ?? (a[key] > b[key] ? -1 : b[key] > a[key] ? 1 : 0)
78
- )
98
+ return compareUndefinedNull(a, b) ?? (a > b ? -1 : b > a ? 1 : 0)
79
99
  }
80
100
  }
81
101
 
@@ -95,140 +115,3 @@ export function multilevel(...comparators) {
95
115
  return 0
96
116
  }
97
117
  }
98
-
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
120
- }
121
-
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
- }
142
-
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,6 +1,5 @@
1
1
  /* eslint-disable no-restricted-syntax */
2
2
  import { describe, expect, it, jest } from "@jest/globals"
3
- import { findClosest } from "./array.js"
4
3
 
5
4
  const { chunk, unique, ascending, descending, multilevel } = await import("./array.js")
6
5
 
@@ -110,6 +109,24 @@ describe("ascending", () => {
110
109
  arr.sort(ascending("v"))
111
110
  expect(arr.map((o) => o.v)).toEqual([1, 2, undefined])
112
111
  })
112
+
113
+ it("sorts using a transform function", () => {
114
+ const arr = [{ v: 2 }, { v: 1 }, { v: 3 }]
115
+ arr.sort(ascending((o) => o.v))
116
+ expect(arr.map((o) => o.v)).toEqual([1, 2, 3])
117
+ })
118
+
119
+ it("sorts using a numeric key", () => {
120
+ const arr = [{ 0: 2 }, { 0: 1 }, { 0: 3 }]
121
+ arr.sort(ascending(0))
122
+ expect(arr.map((o) => o[0])).toEqual([1, 2, 3])
123
+ })
124
+
125
+ it("sorts using a function that returns undefined/null", () => {
126
+ const arr = [{ v: 2 }, {}, { v: 1 }, { v: null }]
127
+ arr.sort(ascending((o) => o.v))
128
+ expect(arr.map((o) => o.v)).toEqual([1, 2, undefined, null])
129
+ })
113
130
  })
114
131
 
115
132
  describe("descending", () => {
@@ -151,6 +168,24 @@ describe("descending", () => {
151
168
  arr.sort(descending("v"))
152
169
  expect(arr.map((o) => o.v)).toEqual([3, 2, undefined])
153
170
  })
171
+
172
+ it("sorts using a transform function", () => {
173
+ const arr = [{ v: 2 }, { v: 1 }, { v: 3 }]
174
+ arr.sort(descending((o) => o.v))
175
+ expect(arr.map((o) => o.v)).toEqual([3, 2, 1])
176
+ })
177
+
178
+ it("sorts using a numeric key", () => {
179
+ const arr = [{ 0: 2 }, { 0: 1 }, { 0: 3 }]
180
+ arr.sort(descending(0))
181
+ expect(arr.map((o) => o[0])).toEqual([3, 2, 1])
182
+ })
183
+
184
+ it("sorts using a function that returns undefined/null", () => {
185
+ const arr = [{ v: 2 }, {}, { v: 1 }, { v: null }]
186
+ arr.sort(descending((o) => o.v))
187
+ expect(arr.map((o) => o.v)).toEqual([2, 1, undefined, null])
188
+ })
154
189
  })
155
190
 
156
191
  describe("multilevel", () => {
@@ -219,153 +254,3 @@ describe("multilevel", () => {
219
254
  expect(cmp("a", "b")).toBe(0)
220
255
  })
221
256
  })
222
-
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()
253
- })
254
-
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()
259
- })
260
-
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
- )
285
- })
286
-
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()
370
- })
371
- })
package/src/find.js ADDED
@@ -0,0 +1,190 @@
1
+ export function findClosestAbs(array, desired, { key, map, threshold = Infinity } = {}) {
2
+ let closest
3
+ if (map) {
4
+ for (let i = 0; i < array.length; i++) {
5
+ const element = array[i]
6
+ const value = map(element, i, array)
7
+ const diff = Math.abs(value - desired)
8
+ if (diff < threshold) {
9
+ closest = element
10
+ threshold = diff
11
+ }
12
+ }
13
+ } else if (key) {
14
+ for (const element of array) {
15
+ const value = element[key]
16
+ const diff = Math.abs(value - desired)
17
+ if (diff < threshold) {
18
+ closest = element
19
+ threshold = diff
20
+ }
21
+ }
22
+ } else {
23
+ for (const value of array) {
24
+ const diff = Math.abs(value - desired)
25
+ if (diff < threshold) {
26
+ closest = value
27
+ threshold = diff
28
+ }
29
+ }
30
+ }
31
+ return closest
32
+ }
33
+
34
+ export function findClosestLT(array, desired, { key, map, threshold = -Infinity } = {}) {
35
+ let closest
36
+ if (map) {
37
+ for (let i = 0; i < array.length; i++) {
38
+ const element = array[i]
39
+ const value = map(element, i, array)
40
+ if (value < desired && value > threshold) {
41
+ closest = element
42
+ threshold = value
43
+ }
44
+ }
45
+ } else if (key) {
46
+ for (const element of array) {
47
+ const value = element[key]
48
+ if (value < desired && value > threshold) {
49
+ closest = element
50
+ threshold = value
51
+ }
52
+ }
53
+ } else {
54
+ for (const value of array) {
55
+ if (value < desired && value > threshold) {
56
+ closest = value
57
+ threshold = value
58
+ }
59
+ }
60
+ }
61
+ return closest
62
+ }
63
+
64
+ export function findClosestLTE(array, desired, { key, map, threshold = -Infinity } = {}) {
65
+ let closest
66
+ if (map) {
67
+ for (let i = 0; i < array.length; i++) {
68
+ const element = array[i]
69
+ const value = map(element, i, array)
70
+ if (value <= desired && value > threshold) {
71
+ closest = element
72
+ threshold = value
73
+ }
74
+ }
75
+ } else if (key) {
76
+ for (const element of array) {
77
+ const value = element[key]
78
+ if (value <= desired && value > threshold) {
79
+ closest = element
80
+ threshold = value
81
+ }
82
+ }
83
+ } else {
84
+ for (const value of array) {
85
+ if (value <= desired && value > threshold) {
86
+ closest = value
87
+ threshold = value
88
+ }
89
+ }
90
+ }
91
+ return closest
92
+ }
93
+
94
+ export function findClosestGT(array, desired, { key, map, threshold = Infinity } = {}) {
95
+ let closest
96
+ if (map) {
97
+ for (let i = 0; i < array.length; i++) {
98
+ const element = array[i]
99
+ const value = map(element, i, array)
100
+ if (value > desired && value < threshold) {
101
+ closest = element
102
+ threshold = value
103
+ }
104
+ }
105
+ } else if (key) {
106
+ for (const element of array) {
107
+ const value = element[key]
108
+ if (value > desired && value < threshold) {
109
+ closest = element
110
+ threshold = value
111
+ }
112
+ }
113
+ } else {
114
+ for (const value of array) {
115
+ if (value > desired && value < threshold) {
116
+ closest = value
117
+ threshold = value
118
+ }
119
+ }
120
+ }
121
+ return closest
122
+ }
123
+
124
+ export function findClosestGTE(array, desired, { key, map, threshold = Infinity } = {}) {
125
+ let closest
126
+ if (map) {
127
+ for (let i = 0; i < array.length; i++) {
128
+ const element = array[i]
129
+ const value = map(element, i, array)
130
+ if (value >= desired && value < threshold) {
131
+ closest = element
132
+ threshold = value
133
+ }
134
+ }
135
+ } else if (key) {
136
+ for (const element of array) {
137
+ const value = element[key]
138
+ if (value >= desired && value < threshold) {
139
+ closest = element
140
+ threshold = value
141
+ }
142
+ }
143
+ } else {
144
+ for (const value of array) {
145
+ if (value >= desired && value < threshold) {
146
+ closest = value
147
+ threshold = value
148
+ }
149
+ }
150
+ }
151
+ return closest
152
+ }
153
+
154
+ /**
155
+ * Find the closest element in an array.
156
+ * If using for strings, need to specify different values for "threshold" and "comparator".
157
+ * "~" and "" are good threshold string values for gt/gte and lt/lte respectively.
158
+ * @template T, V
159
+ * @param {Array<T>} array
160
+ * @param {V} value The desired value to search for
161
+ * @param {Object} options
162
+ * @param {string|number=} options.key If specified, will consider the value for each element's key instead of the element itself.
163
+ * @param {Function=} options.map If specified, will compute value by calling provided function on the element. Takes precedence over key.
164
+ * @param {string|number|Function=} options.transform Allows combining key and map as one parameter. Useful for piping in passed values.
165
+ * @param {string=} options.comparator "abs", "lt", "lte", "gt", "gte", "abs". Default is "abs" which implies T is number.
166
+ * @param {V=} options.threshold If specified, uses a different initial min/max/difference than positive or negative infinity.
167
+ * @returns {T|undefined}
168
+ */
169
+ export function findClosest(array, value, options = {}) {
170
+ const { comparator = "abs", transform } = options
171
+ if (typeof transform === "function") {
172
+ options = { ...options, map: transform }
173
+ } else if (typeof transform === "string" || typeof transform === "number") {
174
+ options = { ...options, key: transform }
175
+ }
176
+ switch (comparator) {
177
+ case "lt":
178
+ return findClosestLT(array, value, options)
179
+ case "lte":
180
+ return findClosestLTE(array, value, options)
181
+ case "gt":
182
+ return findClosestGT(array, value, options)
183
+ case "gte":
184
+ return findClosestGTE(array, value, options)
185
+ case "abs":
186
+ return findClosestAbs(array, value, options)
187
+ default:
188
+ throw new Error(`Unknown comparator: ${comparator}`)
189
+ }
190
+ }
@@ -0,0 +1,335 @@
1
+ import {
2
+ findClosest,
3
+ findClosestAbs,
4
+ findClosestGT,
5
+ findClosestGTE,
6
+ findClosestLT,
7
+ findClosestLTE,
8
+ } from "./find.js"
9
+
10
+ describe("findClosest", () => {
11
+ it("returns the closest value by absolute difference (default comparator)", () => {
12
+ expect(findClosest([1, 5, 10], 6)).toBe(5)
13
+ expect(findClosest([1, 5, 10], 8)).toBe(10)
14
+ expect(findClosest([1, 5, 10], 1)).toBe(1)
15
+ })
16
+
17
+ it("returns undefined if array is empty", () => {
18
+ expect(findClosest([], 10)).toBeUndefined()
19
+ })
20
+
21
+ it("returns the closest object by key (abs)", () => {
22
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
23
+ expect(findClosest(arr, 6, { key: "v" })).toEqual({ v: 5 })
24
+ })
25
+
26
+ it("returns the closest value less than input (lt comparator)", () => {
27
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "lt" })).toBe(5)
28
+ expect(findClosest([1, 3, 5, 7], 1, { comparator: "lt" })).toBeUndefined()
29
+ })
30
+
31
+ it("returns the closest value less than or equal to input (lte comparator)", () => {
32
+ expect(findClosest([1, 3, 5, 7], 5, { comparator: "lte" })).toBe(5)
33
+ expect(findClosest([1, 3, 5, 7], 2, { comparator: "lte" })).toBe(1)
34
+ expect(findClosest([1, 3, 5, 7], 0, { comparator: "lte" })).toBeUndefined()
35
+ })
36
+
37
+ it("returns the closest value greater than input (gt comparator)", () => {
38
+ expect(findClosest([1, 3, 5, 7], 5, { comparator: "gt" })).toBe(7)
39
+ expect(findClosest([1, 3, 5, 7], 7, { comparator: "gt" })).toBeUndefined()
40
+ })
41
+
42
+ it("returns the closest value greater than or equal to input (gte comparator)", () => {
43
+ expect(findClosest([1, 3, 5, 7], 5, { comparator: "gte" })).toBe(5)
44
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "gte" })).toBe(7)
45
+ expect(findClosest([1, 3, 5, 7], 8, { comparator: "gte" })).toBeUndefined()
46
+ })
47
+
48
+ it("returns the closest object by key for lt/lte/gt/gte", () => {
49
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
50
+ expect(findClosest(arr, 6, { comparator: "lt", key: "v" })).toEqual({ v: 5 })
51
+ expect(findClosest(arr, 6, { comparator: "lte", key: "v" })).toEqual({ v: 5 })
52
+ expect(findClosest(arr, 6, { comparator: "gt", key: "v" })).toEqual({ v: 10 })
53
+ expect(findClosest(arr, 10, { comparator: "gte", key: "v" })).toEqual({ v: 10 })
54
+ })
55
+
56
+ it("respects the threshold option for abs comparator", () => {
57
+ expect(findClosest([1, 5, 10], 6, { threshold: 0.5 })).toBeUndefined()
58
+ expect(findClosest([1, 5, 10], 6, { threshold: 2 })).toBe(5)
59
+ })
60
+
61
+ it("respects the threshold option for lt/lte/gt/gte", () => {
62
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "lt", threshold: 4 })).toBe(5)
63
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "lt", threshold: 5 })).toBeUndefined()
64
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "gt", threshold: 7 })).toBeUndefined()
65
+ expect(findClosest([1, 3, 5, 7], 6, { comparator: "gt", threshold: 10 })).toBe(7)
66
+ })
67
+
68
+ it("throws for unknown comparator", () => {
69
+ expect(() => findClosest([1, 2, 3], 2, { comparator: "foo" })).toThrow(
70
+ "Unknown comparator: foo"
71
+ )
72
+ })
73
+
74
+ it("returns undefined if no element matches threshold/key criteria", () => {
75
+ expect(
76
+ findClosest([{ v: 1 }], 10, { comparator: "gt", key: "v", threshold: 1 })
77
+ ).toBeUndefined()
78
+ expect(
79
+ findClosest([{ v: 1 }], 0, { comparator: "lt", key: "v", threshold: 1 })
80
+ ).toBeUndefined()
81
+ })
82
+
83
+ it("works with negative numbers and zero", () => {
84
+ expect(findClosest([-10, -5, 0, 5, 10], -7)).toBe(-5)
85
+ expect(findClosest([-10, -5, 0, 5, 10], 0)).toBe(0)
86
+ expect(findClosest([-10, -5, 0, 5, 10], 7)).toBe(5)
87
+ })
88
+
89
+ // ISSUE: findClosestAbs and related functions do not skip NaN values in key/map modes, only in value mode.
90
+ it("skips NaN values in abs comparator", () => {
91
+ expect(findClosest([1, NaN, 5], 4)).toBe(5)
92
+ })
93
+
94
+ it("skips objects missing the key in key-based comparators", () => {
95
+ const arr = [{ v: 1 }, {}, { v: 5 }]
96
+ expect(findClosest(arr, 2, { key: "v" })).toEqual({ v: 1 })
97
+ })
98
+
99
+ it("finds the closest string using abs comparator and a custom threshold/comparator", () => {
100
+ // Since abs comparator expects numbers, we need to provide a custom comparator for strings.
101
+ // We'll use threshold and comparator: "lt", "lte", "gt", "gte" for string comparisons.
102
+ const arr = ["apple", "banana", "cherry", "date"]
103
+ // Find the closest string less than "carrot" (alphabetically)
104
+ expect(findClosest(arr, "carrot", { comparator: "lt", threshold: "" })).toBe("banana")
105
+ // Find the closest string less than or equal to "banana"
106
+ expect(findClosest(arr, "banana", { comparator: "lte", threshold: "" })).toBe("banana")
107
+ // Find the closest string greater than "carrot"
108
+ expect(findClosest(arr, "carrot", { comparator: "gt", threshold: "~" })).toBe("cherry")
109
+ // Find the closest string greater than or equal to "date"
110
+ expect(findClosest(arr, "date", { comparator: "gte", threshold: "~" })).toBe("date")
111
+ // If nothing matches, returns undefined
112
+ expect(findClosest(arr, "aardvark", { comparator: "lt", threshold: "" })).toBeUndefined()
113
+ expect(findClosest(arr, "zebra", { comparator: "gt", threshold: "~" })).toBeUndefined()
114
+ })
115
+
116
+ it("finds the closest string by key in array of objects", () => {
117
+ const arr = [{ name: "apple" }, { name: "banana" }, { name: "cherry" }]
118
+ expect(
119
+ findClosest(arr, "blueberry", { comparator: "lt", key: "name", threshold: "" })
120
+ ).toEqual({
121
+ name: "banana",
122
+ })
123
+ expect(
124
+ findClosest(arr, "banana", { comparator: "lte", key: "name", threshold: "" })
125
+ ).toEqual({
126
+ name: "banana",
127
+ })
128
+ expect(
129
+ findClosest(arr, "banana", { comparator: "gt", key: "name", threshold: "~" })
130
+ ).toEqual({
131
+ name: "cherry",
132
+ })
133
+ expect(
134
+ findClosest(arr, "cherry", { comparator: "gte", key: "name", threshold: "~" })
135
+ ).toEqual({
136
+ name: "cherry",
137
+ })
138
+ expect(
139
+ findClosest(arr, "aardvark", { comparator: "lt", key: "name", threshold: "" })
140
+ ).toBeUndefined()
141
+ })
142
+
143
+ it("returns undefined if no string matches threshold/key criteria", () => {
144
+ const arr = ["apple", "banana", "cherry"]
145
+ expect(findClosest(arr, "apple", { comparator: "lt", threshold: "" })).toBeUndefined()
146
+ expect(findClosest(arr, "cherry", { comparator: "gt" })).toBeUndefined()
147
+ })
148
+
149
+ it("can use abs comparator with string lengths", () => {
150
+ // This is a reasonable use-case for abs: find string with length closest to 4
151
+ const arr = ["a", "bb", "ccc", "dddd", "eeeee"]
152
+ // Map to string lengths using key
153
+ expect(findClosest(arr, 4, { comparator: "abs", key: "length" })).toEqual("dddd")
154
+ // If threshold is set so no string length is close enough
155
+ expect(
156
+ findClosest(arr, 4, { comparator: "abs", key: "length", threshold: -1 })
157
+ ).toBeUndefined()
158
+ })
159
+
160
+ it("uses transform as a function (same as map)", () => {
161
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
162
+ const mapFn = (el) => el.v
163
+ expect(findClosest(arr, 6, { transform: mapFn })).toEqual({ v: 5 })
164
+ // Should take precedence over key if both are present
165
+ expect(findClosest(arr, 6, { key: "notUsed", transform: mapFn })).toEqual({ v: 5 })
166
+ })
167
+
168
+ it("uses transform as a string (same as key)", () => {
169
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
170
+ expect(findClosest(arr, 6, { transform: "v" })).toEqual({ v: 5 })
171
+ // Should take precedence over key if both are present
172
+ expect(findClosest(arr, 6, { key: "notUsed", transform: "v" })).toEqual({ v: 5 })
173
+ })
174
+
175
+ it("uses transform as a number (same as key)", () => {
176
+ const arr = [[1], [5], [10]]
177
+ expect(findClosest(arr, 6, { transform: 0 })).toEqual([5])
178
+ // Should take precedence over key if both are present
179
+ expect(findClosest(arr, 6, { key: "notUsed", transform: 0 })).toEqual([5])
180
+ })
181
+
182
+ it("transform does not override key if key is already present and transform is not provided", () => {
183
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
184
+ expect(findClosest(arr, 6, { key: "v" })).toEqual({ v: 5 })
185
+ })
186
+
187
+ it("transform is ignored if not a function, string, or number", () => {
188
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
189
+ // eslint-disable-next-line no-restricted-syntax
190
+ expect(findClosest(arr, 6, { transform: null, key: "v" })).toEqual({ v: 5 })
191
+ expect(findClosest(arr, 6, { transform: {}, key: "v" })).toEqual({ v: 5 })
192
+ })
193
+ })
194
+
195
+ describe("findClosestAbs", () => {
196
+ it("returns closest value by absolute difference", () => {
197
+ expect(findClosestAbs([1, 5, 10], 6)).toBe(5)
198
+ expect(findClosestAbs([1, 5, 10], 8)).toBe(10)
199
+ expect(findClosestAbs([1, 5, 10], 1)).toBe(1)
200
+ })
201
+
202
+ it("returns undefined for empty array", () => {
203
+ expect(findClosestAbs([], 10)).toBeUndefined()
204
+ })
205
+
206
+ it("returns closest object by key", () => {
207
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
208
+ expect(findClosestAbs(arr, 6, { key: "v" })).toEqual({ v: 5 })
209
+ })
210
+
211
+ it("returns closest value by map", () => {
212
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
213
+ expect(findClosestAbs(arr, 6, { map: (el) => el.v })).toEqual({ v: 5 })
214
+ })
215
+
216
+ it("respects threshold", () => {
217
+ expect(findClosestAbs([1, 5, 10], 6, { threshold: 0.5 })).toBeUndefined()
218
+ expect(findClosestAbs([1, 5, 10], 6, { threshold: 2 })).toBe(5)
219
+ })
220
+
221
+ it("skips NaN in value mode but not in key/map mode", () => {
222
+ expect(findClosestAbs([1, NaN, 5], 4)).toBe(5)
223
+ const arr = [{ v: 1 }, { v: NaN }, { v: 5 }]
224
+ expect(findClosestAbs(arr, 2, { key: "v" })).toEqual({ v: 1 })
225
+ expect(findClosestAbs(arr, 2, { map: (el) => el.v })).toEqual({ v: 1 })
226
+ })
227
+ })
228
+
229
+ describe("findClosestLT", () => {
230
+ it("returns closest value less than desired", () => {
231
+ expect(findClosestLT([1, 3, 5, 7], 6)).toBe(5)
232
+ expect(findClosestLT([1, 3, 5, 7], 1)).toBeUndefined()
233
+ })
234
+
235
+ it("returns closest object by key", () => {
236
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
237
+ expect(findClosestLT(arr, 6, { key: "v" })).toEqual({ v: 5 })
238
+ })
239
+
240
+ it("returns closest object by map", () => {
241
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
242
+ expect(findClosestLT(arr, 6, { map: (el) => el.v })).toEqual({ v: 5 })
243
+ })
244
+
245
+ it("respects threshold", () => {
246
+ expect(findClosestLT([1, 3, 5, 7], 6, { threshold: 4 })).toBe(5)
247
+ expect(findClosestLT([1, 3, 5, 7], 6, { threshold: 5 })).toBeUndefined()
248
+ })
249
+
250
+ it("returns undefined for empty array", () => {
251
+ expect(findClosestLT([], 10)).toBeUndefined()
252
+ })
253
+ })
254
+
255
+ describe("findClosestLTE", () => {
256
+ it("returns closest value less than or equal to desired", () => {
257
+ expect(findClosestLTE([1, 3, 5, 7], 5)).toBe(5)
258
+ expect(findClosestLTE([1, 3, 5, 7], 2)).toBe(1)
259
+ expect(findClosestLTE([1, 3, 5, 7], 0)).toBeUndefined()
260
+ })
261
+
262
+ it("returns closest object by key", () => {
263
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
264
+ expect(findClosestLTE(arr, 6, { key: "v" })).toEqual({ v: 5 })
265
+ })
266
+
267
+ it("returns closest object by map", () => {
268
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
269
+ expect(findClosestLTE(arr, 6, { map: (el) => el.v })).toEqual({ v: 5 })
270
+ })
271
+
272
+ it("respects threshold", () => {
273
+ expect(findClosestLTE([1, 3, 5, 7], 6, { threshold: 4 })).toBe(5)
274
+ expect(findClosestLTE([1, 3, 5, 7], 6, { threshold: 5 })).toBeUndefined()
275
+ })
276
+
277
+ it("returns undefined for empty array", () => {
278
+ expect(findClosestLTE([], 10)).toBeUndefined()
279
+ })
280
+ })
281
+
282
+ describe("findClosestGT", () => {
283
+ it("returns closest value greater than desired", () => {
284
+ expect(findClosestGT([1, 3, 5, 7], 5)).toBe(7)
285
+ expect(findClosestGT([1, 3, 5, 7], 7)).toBeUndefined()
286
+ })
287
+
288
+ it("returns closest object by key", () => {
289
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
290
+ expect(findClosestGT(arr, 6, { key: "v" })).toEqual({ v: 10 })
291
+ })
292
+
293
+ it("returns closest object by map", () => {
294
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
295
+ expect(findClosestGT(arr, 6, { map: (el) => el.v })).toEqual({ v: 10 })
296
+ })
297
+
298
+ it("respects threshold", () => {
299
+ expect(findClosestGT([1, 3, 5, 7], 6, { threshold: 7 })).toBeUndefined()
300
+ expect(findClosestGT([1, 3, 5, 7], 6, { threshold: 10 })).toBe(7)
301
+ })
302
+
303
+ it("returns undefined for empty array", () => {
304
+ expect(findClosestGT([], 10)).toBeUndefined()
305
+ })
306
+ })
307
+
308
+ describe("findClosestGTE", () => {
309
+ it("returns closest value greater than or equal to desired", () => {
310
+ expect(findClosestGTE([1, 3, 5, 7], 5)).toBe(5)
311
+ expect(findClosestGTE([1, 3, 5, 7], 6)).toBe(7)
312
+ expect(findClosestGTE([1, 3, 5, 7], 8)).toBeUndefined()
313
+ })
314
+
315
+ it("returns closest object by key", () => {
316
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
317
+ expect(findClosestGTE(arr, 6, { key: "v" })).toEqual({ v: 10 })
318
+ expect(findClosestGTE(arr, 10, { key: "v" })).toEqual({ v: 10 })
319
+ })
320
+
321
+ it("returns closest object by map", () => {
322
+ const arr = [{ v: 1 }, { v: 5 }, { v: 10 }]
323
+ expect(findClosestGTE(arr, 6, { map: (el) => el.v })).toEqual({ v: 10 })
324
+ expect(findClosestGTE(arr, 10, { map: (el) => el.v })).toEqual({ v: 10 })
325
+ })
326
+
327
+ it("respects threshold", () => {
328
+ expect(findClosestGTE([1, 3, 5, 7], 6, { threshold: 7 })).toBeUndefined()
329
+ expect(findClosestGTE([1, 3, 5, 7], 6, { threshold: 10 })).toBe(7)
330
+ })
331
+
332
+ it("returns undefined for empty array", () => {
333
+ expect(findClosestGTE([], 10)).toBeUndefined()
334
+ })
335
+ })
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./array.js"
2
+ export { findClosest } from "./find.js"
2
3
  export * from "./math.js"
3
4
  export * from "./object.js"
4
5
  export * from "./promise.js"
package/src/time.js CHANGED
@@ -152,7 +152,7 @@ export function getTimeRange(start, end, { hours = 0, minutes = 1 } = {}) {
152
152
  * @param {number} days
153
153
  * @returns {string}
154
154
  */
155
- export function addDays(dateString, days = 0) {
155
+ export function addDays(dateString, { days = 0 } = {}) {
156
156
  const [year, month, day] = dateString.split("-").map(Number)
157
157
  const date = new Date(Date.UTC(year, month - 1, day))
158
158
  date.setUTCDate(date.getUTCDate() + days)
@@ -169,7 +169,7 @@ export function getDateRange(start, end, { limit = 1000 } = {}) {
169
169
  const dates = []
170
170
  while (start <= end) {
171
171
  dates.push(start)
172
- start = addDays(start, 1)
172
+ start = addDays(start, { days: 1 })
173
173
  if (dates.length >= limit) {
174
174
  break
175
175
  }
package/src/time.test.js CHANGED
@@ -364,53 +364,49 @@ describe("getTimeRange", () => {
364
364
  })
365
365
 
366
366
  describe("addDays", () => {
367
- test("adds days within the same month", () => {
368
- expect(addDays("2024-06-01", 5)).toBe("2024-06-06")
369
- expect(addDays("2024-06-10", 0)).toBe("2024-06-10")
367
+ test("adds days within the same month (object param)", () => {
368
+ expect(addDays("2024-06-01", { days: 5 })).toBe("2024-06-06")
369
+ expect(addDays("2024-06-10", { days: 0 })).toBe("2024-06-10")
370
370
  })
371
371
 
372
- test("adds days with month rollover", () => {
373
- expect(addDays("2024-06-28", 5)).toBe("2024-07-03")
372
+ test("adds days with month rollover (object param)", () => {
373
+ expect(addDays("2024-06-28", { days: 5 })).toBe("2024-07-03")
374
374
  })
375
375
 
376
- test("adds days with year rollover", () => {
377
- expect(addDays("2024-12-30", 5)).toBe("2025-01-04")
376
+ test("adds days with year rollover (object param)", () => {
377
+ expect(addDays("2024-12-30", { days: 5 })).toBe("2025-01-04")
378
378
  })
379
379
 
380
- test("subtracts days", () => {
381
- expect(addDays("2024-06-10", -10)).toBe("2024-05-31")
380
+ test("subtracts days (object param)", () => {
381
+ expect(addDays("2024-06-10", { days: -10 })).toBe("2024-05-31")
382
382
  })
383
383
 
384
- test("handles leap years", () => {
385
- expect(addDays("2024-02-28", 1)).toBe("2024-02-29")
386
- expect(addDays("2024-02-28", 2)).toBe("2024-03-01")
387
- expect(addDays("2023-02-28", 1)).toBe("2023-03-01")
384
+ test("handles leap years (object param)", () => {
385
+ expect(addDays("2024-02-28", { days: 1 })).toBe("2024-02-29")
386
+ expect(addDays("2024-02-28", { days: 2 })).toBe("2024-03-01")
387
+ expect(addDays("2023-02-28", { days: 1 })).toBe("2023-03-01")
388
388
  })
389
389
 
390
- test("handles negative result across year boundary", () => {
391
- expect(addDays("2024-01-01", -1)).toBe("2023-12-31")
390
+ test("handles negative result across year boundary (object param)", () => {
391
+ expect(addDays("2024-01-01", { days: -1 })).toBe("2023-12-31")
392
392
  })
393
393
 
394
394
  // DST boundary: adding days across US DST start (spring forward)
395
- test("adds days across DST start (spring forward)", () => {
396
- // DST starts in US/Eastern on 2024-03-10
397
- // Adding 1 day to 2024-03-09 should yield 2024-03-10
398
- expect(addDays("2024-03-09", 1)).toBe("2024-03-10")
399
- // Adding 2 days to 2024-03-09 should yield 2024-03-11
400
- expect(addDays("2024-03-09", 2)).toBe("2024-03-11")
401
- // Subtracting 1 day from 2024-03-10 should yield 2024-03-09
402
- expect(addDays("2024-03-10", -1)).toBe("2024-03-09")
395
+ test("adds days across DST start (spring forward) (object param)", () => {
396
+ expect(addDays("2024-03-09", { days: 1 })).toBe("2024-03-10")
397
+ expect(addDays("2024-03-09", { days: 2 })).toBe("2024-03-11")
398
+ expect(addDays("2024-03-10", { days: -1 })).toBe("2024-03-09")
403
399
  })
404
400
 
405
401
  // DST boundary: adding days across US DST end (fall back)
406
- test("adds days across DST end (fall back)", () => {
407
- // DST ends in US/Eastern on 2024-11-03
408
- // Adding 1 day to 2024-11-02 should yield 2024-11-03
409
- expect(addDays("2024-11-02", 1)).toBe("2024-11-03")
410
- // Adding 2 days to 2024-11-02 should yield 2024-11-04
411
- expect(addDays("2024-11-02", 2)).toBe("2024-11-04")
412
- // Subtracting 1 day from 2024-11-03 should yield 2024-11-02
413
- expect(addDays("2024-11-03", -1)).toBe("2024-11-02")
402
+ test("adds days across DST end (fall back) (object param)", () => {
403
+ expect(addDays("2024-11-02", { days: 1 })).toBe("2024-11-03")
404
+ expect(addDays("2024-11-02", { days: 2 })).toBe("2024-11-04")
405
+ expect(addDays("2024-11-03", { days: -1 })).toBe("2024-11-02")
406
+ })
407
+
408
+ test("accepts missing options argument (defaults to days = 0)", () => {
409
+ expect(addDays("2024-06-10")).toBe("2024-06-10")
414
410
  })
415
411
  })
416
412