@tim-code/my-util 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/find.js +142 -63
  3. package/src/find.test.js +199 -248
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tim-code/my-util",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "",
package/src/find.js CHANGED
@@ -1,150 +1,150 @@
1
- export function findClosestAbs(array, desired, { key, map, threshold = Infinity } = {}) {
1
+ export function findClosestAbs(array, desired, { key, cutoff = Infinity } = {}) {
2
2
  let closest
3
- if (map) {
3
+ if (typeof key === "function") {
4
4
  for (let i = 0; i < array.length; i++) {
5
5
  const element = array[i]
6
- const value = map(element, i, array)
6
+ const value = key(element, i, array)
7
7
  const diff = Math.abs(value - desired)
8
- if (diff < threshold) {
8
+ if (diff < cutoff) {
9
9
  closest = element
10
- threshold = diff
10
+ cutoff = diff
11
11
  }
12
12
  }
13
- } else if (key) {
13
+ } else if (typeof key === "number" || typeof key === "string") {
14
14
  for (const element of array) {
15
15
  const value = element[key]
16
16
  const diff = Math.abs(value - desired)
17
- if (diff < threshold) {
17
+ if (diff < cutoff) {
18
18
  closest = element
19
- threshold = diff
19
+ cutoff = diff
20
20
  }
21
21
  }
22
22
  } else {
23
23
  for (const value of array) {
24
24
  const diff = Math.abs(value - desired)
25
- if (diff < threshold) {
25
+ if (diff < cutoff) {
26
26
  closest = value
27
- threshold = diff
27
+ cutoff = diff
28
28
  }
29
29
  }
30
30
  }
31
31
  return closest
32
32
  }
33
33
 
34
- export function findClosestLT(array, desired, { key, map, threshold = -Infinity } = {}) {
34
+ export function findClosestLT(array, desired, { key, cutoff = -Infinity } = {}) {
35
35
  let closest
36
- if (map) {
36
+ if (typeof key === "function") {
37
37
  for (let i = 0; i < array.length; i++) {
38
38
  const element = array[i]
39
- const value = map(element, i, array)
40
- if (value < desired && value > threshold) {
39
+ const value = key(element, i, array)
40
+ if (value < desired && value > cutoff) {
41
41
  closest = element
42
- threshold = value
42
+ cutoff = value
43
43
  }
44
44
  }
45
- } else if (key) {
45
+ } else if (typeof key === "number" || typeof key === "string") {
46
46
  for (const element of array) {
47
47
  const value = element[key]
48
- if (value < desired && value > threshold) {
48
+ if (value < desired && value > cutoff) {
49
49
  closest = element
50
- threshold = value
50
+ cutoff = value
51
51
  }
52
52
  }
53
53
  } else {
54
54
  for (const value of array) {
55
- if (value < desired && value > threshold) {
55
+ if (value < desired && value > cutoff) {
56
56
  closest = value
57
- threshold = value
57
+ cutoff = value
58
58
  }
59
59
  }
60
60
  }
61
61
  return closest
62
62
  }
63
63
 
64
- export function findClosestLTE(array, desired, { key, map, threshold = -Infinity } = {}) {
64
+ export function findClosestLTE(array, desired, { key, cutoff = -Infinity } = {}) {
65
65
  let closest
66
- if (map) {
66
+ if (typeof key === "function") {
67
67
  for (let i = 0; i < array.length; i++) {
68
68
  const element = array[i]
69
- const value = map(element, i, array)
70
- if (value <= desired && value > threshold) {
69
+ const value = key(element, i, array)
70
+ if (value <= desired && value > cutoff) {
71
71
  closest = element
72
- threshold = value
72
+ cutoff = value
73
73
  }
74
74
  }
75
- } else if (key) {
75
+ } else if (typeof key === "number" || typeof key === "string") {
76
76
  for (const element of array) {
77
77
  const value = element[key]
78
- if (value <= desired && value > threshold) {
78
+ if (value <= desired && value > cutoff) {
79
79
  closest = element
80
- threshold = value
80
+ cutoff = value
81
81
  }
82
82
  }
83
83
  } else {
84
84
  for (const value of array) {
85
- if (value <= desired && value > threshold) {
85
+ if (value <= desired && value > cutoff) {
86
86
  closest = value
87
- threshold = value
87
+ cutoff = value
88
88
  }
89
89
  }
90
90
  }
91
91
  return closest
92
92
  }
93
93
 
94
- export function findClosestGT(array, desired, { key, map, threshold = Infinity } = {}) {
94
+ export function findClosestGT(array, desired, { key, cutoff = Infinity } = {}) {
95
95
  let closest
96
- if (map) {
96
+ if (typeof key === "function") {
97
97
  for (let i = 0; i < array.length; i++) {
98
98
  const element = array[i]
99
- const value = map(element, i, array)
100
- if (value > desired && value < threshold) {
99
+ const value = key(element, i, array)
100
+ if (value > desired && value < cutoff) {
101
101
  closest = element
102
- threshold = value
102
+ cutoff = value
103
103
  }
104
104
  }
105
- } else if (key) {
105
+ } else if (typeof key === "number" || typeof key === "string") {
106
106
  for (const element of array) {
107
107
  const value = element[key]
108
- if (value > desired && value < threshold) {
108
+ if (value > desired && value < cutoff) {
109
109
  closest = element
110
- threshold = value
110
+ cutoff = value
111
111
  }
112
112
  }
113
113
  } else {
114
114
  for (const value of array) {
115
- if (value > desired && value < threshold) {
115
+ if (value > desired && value < cutoff) {
116
116
  closest = value
117
- threshold = value
117
+ cutoff = value
118
118
  }
119
119
  }
120
120
  }
121
121
  return closest
122
122
  }
123
123
 
124
- export function findClosestGTE(array, desired, { key, map, threshold = Infinity } = {}) {
124
+ export function findClosestGTE(array, desired, { key, cutoff = Infinity } = {}) {
125
125
  let closest
126
- if (map) {
126
+ if (typeof key === "function") {
127
127
  for (let i = 0; i < array.length; i++) {
128
128
  const element = array[i]
129
- const value = map(element, i, array)
130
- if (value >= desired && value < threshold) {
129
+ const value = key(element, i, array)
130
+ if (value >= desired && value < cutoff) {
131
131
  closest = element
132
- threshold = value
132
+ cutoff = value
133
133
  }
134
134
  }
135
- } else if (key) {
135
+ } else if (typeof key === "number" || typeof key === "string") {
136
136
  for (const element of array) {
137
137
  const value = element[key]
138
- if (value >= desired && value < threshold) {
138
+ if (value >= desired && value < cutoff) {
139
139
  closest = element
140
- threshold = value
140
+ cutoff = value
141
141
  }
142
142
  }
143
143
  } else {
144
144
  for (const value of array) {
145
- if (value >= desired && value < threshold) {
145
+ if (value >= desired && value < cutoff) {
146
146
  closest = value
147
- threshold = value
147
+ cutoff = value
148
148
  }
149
149
  }
150
150
  }
@@ -152,27 +152,24 @@ export function findClosestGTE(array, desired, { key, map, threshold = Infinity
152
152
  }
153
153
 
154
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.
155
+ * Find the closest element in an array. If there is a tie, then returns the first matching element by order in the array.
156
+ * If using for strings, need to specify different values for "cutoff" and "comparator".
157
+ * "~" and "" are good cutoff string values for gt/gte and lt/lte respectively.
158
158
  * @template T, V
159
159
  * @param {Array<T>} array
160
160
  * @param {V} value The desired value to search for
161
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.
162
+ * @param {string|number|Function=} options.key
163
+ * If specified, will consider the value for each element's key instead of the element itself.
164
+ * If a function, called with the element, index and array (same as .map() callback) to produce the value to sort on.
165
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.
166
+ * @param {V=} options.cutoff If specified, sets a initial constraint on how close the found value must be.
167
+ * For example, if used with "lt", the found element would need to be greater than the cutoff but still less than the desired value.
168
+ * If used with "abs", the found element would need to have a difference with the desired value less than the cutoff.
167
169
  * @returns {T|undefined}
168
170
  */
169
171
  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
- }
172
+ const { comparator = "abs" } = options
176
173
  switch (comparator) {
177
174
  case "lt":
178
175
  return findClosestLT(array, value, options)
@@ -188,3 +185,85 @@ export function findClosest(array, value, options = {}) {
188
185
  throw new Error(`Unknown comparator: ${comparator}`)
189
186
  }
190
187
  }
188
+
189
+ /**
190
+ * Find the minimum value in an array.
191
+ * @template T, V
192
+ * @param {Array<T>} array
193
+ * @param {Object} $1
194
+ * @param {string|Function=} $1.key Specifies an alternative to using each element as the value.
195
+ * If string, then accesses each element at that key to get value.
196
+ * If function, then calls the callback on each element to get value.
197
+ * @param {V=} $1.cutoff Only values below cutoff will be considered.
198
+ * @returns {T}
199
+ */
200
+ export function findMin(array, { key, cutoff = Infinity } = {}) {
201
+ let closest
202
+ if (typeof key === "function") {
203
+ for (let i = 0; i < array.length; i++) {
204
+ const element = array[i]
205
+ const value = key(element, i, array)
206
+ if (value < cutoff) {
207
+ closest = element
208
+ cutoff = value
209
+ }
210
+ }
211
+ } else if (typeof key === "number" || typeof key === "string") {
212
+ for (const element of array) {
213
+ const value = element[key]
214
+ if (value < cutoff) {
215
+ closest = element
216
+ cutoff = value
217
+ }
218
+ }
219
+ } else {
220
+ for (const value of array) {
221
+ if (value < cutoff) {
222
+ closest = value
223
+ cutoff = value
224
+ }
225
+ }
226
+ }
227
+ return closest
228
+ }
229
+
230
+ /**
231
+ * Find the maximum value in an array.
232
+ * @template T, V
233
+ * @param {Array<T>} array
234
+ * @param {Object} $1
235
+ * @param {string|Function=} $1.key Specifies an alternative to using each element as the value.
236
+ * If string, then accesses each element at that key to get value.
237
+ * If function, then calls the callback on each element to get value.
238
+ * @param {V=} $1.cutoff Only values above cutoff will be considered.
239
+ * @returns {T}
240
+ */
241
+ export function findMax(array, { key, cutoff = -Infinity } = {}) {
242
+ let closest
243
+ if (typeof key === "function") {
244
+ for (let i = 0; i < array.length; i++) {
245
+ const element = array[i]
246
+ const value = key(element, i, array)
247
+ if (value > cutoff) {
248
+ closest = element
249
+ cutoff = value
250
+ }
251
+ }
252
+ } else if (typeof key === "number" || typeof key === "string") {
253
+ for (const element of array) {
254
+ const value = element[key]
255
+ if (value > cutoff) {
256
+ closest = element
257
+ cutoff = value
258
+ }
259
+ }
260
+ } else {
261
+ for (const value of array) {
262
+ if (value > cutoff) {
263
+ closest = value
264
+ cutoff = value
265
+ }
266
+ }
267
+ }
268
+ return closest
269
+ }
package/src/find.test.js CHANGED
@@ -1,335 +1,286 @@
1
- import {
2
- findClosest,
1
+ import { describe, expect, it } from "@jest/globals"
2
+
3
+ const {
3
4
  findClosestAbs,
4
- findClosestGT,
5
- findClosestGTE,
6
5
  findClosestLT,
7
6
  findClosestLTE,
8
- } from "./find.js"
7
+ findClosestGT,
8
+ findClosestGTE,
9
+ findClosest,
10
+ findMin,
11
+ findMax,
12
+ } = await import("./find.js")
9
13
 
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)
14
+ describe("findClosestAbs", () => {
15
+ it("returns the element closest in absolute value to desired", () => {
16
+ expect(findClosestAbs([1, 5, 9], 6)).toBe(5)
17
+ expect(findClosestAbs([1, 5, 9], 8)).toBe(9)
18
+ expect(findClosestAbs([1, 5, 9], 1)).toBe(1)
15
19
  })
16
20
 
17
- it("returns undefined if array is empty", () => {
18
- expect(findClosest([], 10)).toBeUndefined()
21
+ it("returns the first element in case of tie", () => {
22
+ expect(findClosestAbs([4, 8], 6)).toBe(4)
19
23
  })
20
24
 
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 })
25
+ it("returns undefined for empty array", () => {
26
+ expect(findClosestAbs([], 10)).toBeUndefined()
24
27
  })
25
28
 
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
+ it("supports key as function", () => {
30
+ const arr = [{ v: 2 }, { v: 8 }]
31
+ expect(findClosestAbs(arr, 5, { key: (e) => e.v })).toEqual({ v: 2 })
29
32
  })
30
33
 
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()
34
+ it("supports key as string", () => {
35
+ const arr = [{ x: 1 }, { x: 10 }]
36
+ expect(findClosestAbs(arr, 8, { key: "x" })).toEqual({ x: 10 })
35
37
  })
36
38
 
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()
39
+ it("supports key as number", () => {
40
+ const arr = [[2], [8]]
41
+ expect(findClosestAbs(arr, 7, { key: 0 })).toEqual([8])
40
42
  })
41
43
 
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()
44
+ it("respects cutoff", () => {
45
+ expect(findClosestAbs([1, 5, 9], 6, { cutoff: 2 })).toBe(5)
46
+ expect(findClosestAbs([1, 5, 9], 6, { cutoff: 1 })).toBeUndefined()
46
47
  })
48
+ })
47
49
 
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 })
50
+ describe("findClosestLT", () => {
51
+ it("returns the closest element less than desired", () => {
52
+ expect(findClosestLT([1, 5, 9], 6)).toBe(5)
53
+ expect(findClosestLT([1, 5, 9], 2)).toBe(1)
54
+ expect(findClosestLT([1, 5, 9], 1)).toBeUndefined()
54
55
  })
55
56
 
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)
57
+ it("returns first match if tie", () => {
58
+ expect(findClosestLT([2, 2, 1], 3)).toBe(2)
59
59
  })
60
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)
61
+ it("returns undefined for empty array", () => {
62
+ expect(findClosestLT([], 10)).toBeUndefined()
66
63
  })
67
64
 
68
- it("throws for unknown comparator", () => {
69
- expect(() => findClosest([1, 2, 3], 2, { comparator: "foo" })).toThrow(
70
- "Unknown comparator: foo"
71
- )
65
+ it("supports key as function", () => {
66
+ const arr = [{ v: 2 }, { v: 8 }]
67
+ expect(findClosestLT(arr, 8, { key: (e) => e.v })).toEqual({ v: 2 })
68
+ })
69
+
70
+ it("supports key as string", () => {
71
+ const arr = [{ x: 1 }, { x: 10 }]
72
+ expect(findClosestLT(arr, 8, { key: "x" })).toEqual({ x: 1 })
72
73
  })
73
74
 
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 })
75
+ it("supports key as number", () => {
76
+ const arr = [[2], [8]]
77
+ expect(findClosestLT(arr, 7, { key: 0 })).toEqual([2])
78
+ })
79
+
80
+ it("respects cutoff", () => {
81
+ expect(findClosestLT([1, 5, 9], 6, { cutoff: 4 })).toBe(5)
82
+ expect(findClosestLT([1, 5, 9], 6, { cutoff: 5 })).toBeUndefined()
192
83
  })
193
84
  })
194
85
 
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)
86
+ describe("findClosestLTE", () => {
87
+ it("returns the closest element less than or equal to desired", () => {
88
+ expect(findClosestLTE([1, 5, 9], 5)).toBe(5)
89
+ expect(findClosestLTE([1, 5, 9], 6)).toBe(5)
90
+ expect(findClosestLTE([1, 5, 9], 1)).toBe(1)
91
+ expect(findClosestLTE([1, 5, 9], 0)).toBeUndefined()
92
+ })
93
+
94
+ it("returns first match if tie", () => {
95
+ expect(findClosestLTE([2, 2, 1], 2)).toBe(2)
200
96
  })
201
97
 
202
98
  it("returns undefined for empty array", () => {
203
- expect(findClosestAbs([], 10)).toBeUndefined()
99
+ expect(findClosestLTE([], 10)).toBeUndefined()
204
100
  })
205
101
 
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 })
102
+ it("supports key as function", () => {
103
+ const arr = [{ v: 2 }, { v: 8 }]
104
+ expect(findClosestLTE(arr, 8, { key: (e) => e.v })).toEqual({ v: 8 })
209
105
  })
210
106
 
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 })
107
+ it("supports key as string", () => {
108
+ const arr = [{ x: 1 }, { x: 10 }]
109
+ expect(findClosestLTE(arr, 8, { key: "x" })).toEqual({ x: 1 })
214
110
  })
215
111
 
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)
112
+ it("supports key as number", () => {
113
+ const arr = [[2], [8]]
114
+ expect(findClosestLTE(arr, 7, { key: 0 })).toEqual([2])
219
115
  })
220
116
 
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 })
117
+ it("respects cutoff", () => {
118
+ expect(findClosestLTE([1, 5, 9], 6, { cutoff: 4 })).toBe(5)
119
+ expect(findClosestLTE([1, 5, 9], 6, { cutoff: 5 })).toBeUndefined()
226
120
  })
227
121
  })
228
122
 
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()
123
+ describe("findClosestGT", () => {
124
+ it("returns the closest element greater than desired", () => {
125
+ expect(findClosestGT([1, 5, 9], 6)).toBe(9)
126
+ expect(findClosestGT([1, 5, 9], 0)).toBe(1)
127
+ expect(findClosestGT([1, 5, 9], 9)).toBeUndefined()
233
128
  })
234
129
 
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 })
130
+ it("returns first match if tie", () => {
131
+ expect(findClosestGT([8, 8, 10], 7)).toBe(8)
238
132
  })
239
133
 
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 })
134
+ it("returns undefined for empty array", () => {
135
+ expect(findClosestGT([], 10)).toBeUndefined()
243
136
  })
244
137
 
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()
138
+ it("supports key as function", () => {
139
+ const arr = [{ v: 2 }, { v: 8 }]
140
+ expect(findClosestGT(arr, 2, { key: (e) => e.v })).toEqual({ v: 8 })
248
141
  })
249
142
 
250
- it("returns undefined for empty array", () => {
251
- expect(findClosestLT([], 10)).toBeUndefined()
143
+ it("supports key as string", () => {
144
+ const arr = [{ x: 1 }, { x: 10 }]
145
+ expect(findClosestGT(arr, 8, { key: "x" })).toEqual({ x: 10 })
252
146
  })
253
- })
254
147
 
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()
148
+ it("supports key as number", () => {
149
+ const arr = [[2], [8]]
150
+ expect(findClosestGT(arr, 2, { key: 0 })).toEqual([8])
260
151
  })
261
152
 
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 })
153
+ it("respects cutoff", () => {
154
+ expect(findClosestGT([1, 5, 9], 6, { cutoff: 8 })).toBeUndefined()
155
+ expect(findClosestGT([1, 5, 9], 4, { cutoff: 8 })).toBe(5)
265
156
  })
157
+ })
266
158
 
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 })
159
+ describe("findClosestGTE", () => {
160
+ it("returns the closest element greater than or equal to desired", () => {
161
+ expect(findClosestGTE([1, 5, 9], 5)).toBe(5)
162
+ expect(findClosestGTE([1, 5, 9], 4)).toBe(5)
163
+ expect(findClosestGTE([1, 5, 9], 10)).toBeUndefined()
270
164
  })
271
165
 
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()
166
+ it("returns first match if tie", () => {
167
+ expect(findClosestGTE([8, 8, 10], 8)).toBe(8)
275
168
  })
276
169
 
277
170
  it("returns undefined for empty array", () => {
278
- expect(findClosestLTE([], 10)).toBeUndefined()
171
+ expect(findClosestGTE([], 10)).toBeUndefined()
172
+ })
173
+
174
+ it("supports key as function", () => {
175
+ const arr = [{ v: 2 }, { v: 8 }]
176
+ expect(findClosestGTE(arr, 2, { key: (e) => e.v })).toEqual({ v: 2 })
177
+ })
178
+
179
+ it("supports key as string", () => {
180
+ const arr = [{ x: 1 }, { x: 10 }]
181
+ expect(findClosestGTE(arr, 8, { key: "x" })).toEqual({ x: 10 })
182
+ })
183
+
184
+ it("supports key as number", () => {
185
+ const arr = [[2], [8]]
186
+ expect(findClosestGTE(arr, 2, { key: 0 })).toEqual([2])
187
+ })
188
+
189
+ it("respects cutoff", () => {
190
+ expect(findClosestGTE([1, 5, 9], 6, { cutoff: 8 })).toBeUndefined()
191
+ expect(findClosestGTE([1, 5, 9], 4, { cutoff: 8 })).toBe(5)
279
192
  })
280
193
  })
281
194
 
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()
195
+ describe("findClosest", () => {
196
+ it("defaults to abs comparator", () => {
197
+ expect(findClosest([1, 5, 9], 6)).toBe(5)
286
198
  })
287
199
 
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 })
200
+ it("calls correct comparator", () => {
201
+ expect(findClosest([1, 5, 9], 6, { comparator: "lt" })).toBe(5)
202
+ expect(findClosest([1, 5, 9], 6, { comparator: "lte" })).toBe(5)
203
+ expect(findClosest([1, 5, 9], 6, { comparator: "gt" })).toBe(9)
204
+ expect(findClosest([1, 5, 9], 6, { comparator: "gte" })).toBe(9)
205
+ expect(findClosest([1, 5, 9], 6, { comparator: "abs" })).toBe(5)
291
206
  })
292
207
 
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 })
208
+ it("throws on unknown comparator", () => {
209
+ expect(() => findClosest([1, 5, 9], 6, { comparator: "foo" })).toThrow(
210
+ "Unknown comparator: foo"
211
+ )
296
212
  })
297
213
 
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)
214
+ it("passes options to underlying function", () => {
215
+ const arr = [{ x: 1 }, { x: 10 }]
216
+ expect(findClosest(arr, 8, { comparator: "abs", key: "x" })).toEqual({ x: 10 })
217
+ })
218
+ })
219
+
220
+ describe("findMin", () => {
221
+ it("returns the minimum value in a numeric array", () => {
222
+ expect(findMin([3, 1, 4, 2])).toBe(1)
301
223
  })
302
224
 
303
225
  it("returns undefined for empty array", () => {
304
- expect(findClosestGT([], 10)).toBeUndefined()
226
+ expect(findMin([])).toBeUndefined()
305
227
  })
306
- })
307
228
 
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()
229
+ it("returns the first minimum if there are duplicates", () => {
230
+ expect(findMin([2, 1, 1, 3])).toBe(1)
313
231
  })
314
232
 
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 })
233
+ it("supports key as function", () => {
234
+ const arr = [{ v: 5 }, { v: 2 }, { v: 8 }]
235
+ expect(findMin(arr, { key: (e) => e.v })).toEqual({ v: 2 })
319
236
  })
320
237
 
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 })
238
+ it("supports key as string", () => {
239
+ const arr = [{ x: 5 }, { x: 2 }, { x: 8 }]
240
+ expect(findMin(arr, { key: "x" })).toEqual({ x: 2 })
325
241
  })
326
242
 
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)
243
+ it("supports key as number", () => {
244
+ const arr = [[5], [2], [8]]
245
+ expect(findMin(arr, { key: 0 })).toEqual([2])
246
+ })
247
+
248
+ it("respects cutoff", () => {
249
+ expect(findMin([3, 1, 4, 2], { cutoff: 2 })).toBe(1)
250
+ expect(findMin([3, 1, 4, 2], { cutoff: 1 })).toBeUndefined()
251
+ })
252
+ })
253
+
254
+ describe("findMax", () => {
255
+ it("returns the maximum value in a numeric array", () => {
256
+ expect(findMax([3, 1, 4, 2])).toBe(4)
330
257
  })
331
258
 
332
259
  it("returns undefined for empty array", () => {
333
- expect(findClosestGTE([], 10)).toBeUndefined()
260
+ expect(findMax([])).toBeUndefined()
261
+ })
262
+
263
+ it("returns the first maximum if there are duplicates", () => {
264
+ expect(findMax([4, 2, 4, 1])).toBe(4)
265
+ })
266
+
267
+ it("supports key as function", () => {
268
+ const arr = [{ v: 5 }, { v: 2 }, { v: 8 }]
269
+ expect(findMax(arr, { key: (e) => e.v })).toEqual({ v: 8 })
270
+ })
271
+
272
+ it("supports key as string", () => {
273
+ const arr = [{ x: 5 }, { x: 2 }, { x: 8 }]
274
+ expect(findMax(arr, { key: "x" })).toEqual({ x: 8 })
275
+ })
276
+
277
+ it("supports key as number", () => {
278
+ const arr = [[5], [2], [8]]
279
+ expect(findMax(arr, { key: 0 })).toEqual([8])
280
+ })
281
+
282
+ it("respects cutoff", () => {
283
+ expect(findMax([3, 1, 4, 2], { cutoff: 2 })).toBe(4)
284
+ expect(findMax([3, 1, 4, 2], { cutoff: 4 })).toBeUndefined()
334
285
  })
335
286
  })