@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 +1 -1
- package/src/array.js +32 -149
- package/src/array.test.js +36 -151
- package/src/find.js +190 -0
- package/src/find.test.js +335 -0
- package/src/index.js +1 -0
- package/src/time.js +2 -2
- package/src/time.test.js +27 -31
package/package.json
CHANGED
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 {
|
|
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(
|
|
51
|
-
if (
|
|
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 {
|
|
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(
|
|
70
|
-
if (
|
|
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
|
+
}
|
package/src/find.test.js
ADDED
|
@@ -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
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
|
-
|
|
397
|
-
|
|
398
|
-
expect(addDays("2024-03-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
expect(addDays("2024-11-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
expect(addDays("2024-
|
|
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
|
|