@tim-code/my-util 0.0.24 → 0.1.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.
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/array.js +133 -37
- package/src/array.test.js +155 -57
- package/src/index.js +1 -0
- package/src/math.js +14 -0
- package/src/math.test.js +47 -1
- package/src/object.js +57 -0
- package/src/object.test.js +161 -0
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
This library includes common util functions. It does not have any dependencies.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
In combination with Object.groupBy (available when using Node 22), this should be sufficient for replacing the most useful functionality from lodash.
|
|
6
6
|
|
|
7
7
|
## First Time Setup
|
|
8
8
|
|
|
9
|
-
`npm install`
|
|
9
|
+
`npm install @tim-code/my-util`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tim-code/my-util",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
]
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@tim-code/eslint-config": "^1.
|
|
25
|
+
"@tim-code/eslint-config": "^1.3.3",
|
|
26
26
|
"@jest/globals": "^29.7.0",
|
|
27
27
|
"jest": "^29.7.0"
|
|
28
28
|
},
|
package/src/array.js
CHANGED
|
@@ -28,19 +28,6 @@ export function unique(array) {
|
|
|
28
28
|
return [...new Set(array)]
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/**
|
|
32
|
-
* Mutates the passed in object by calling callback on each of its values.
|
|
33
|
-
* @param {Object} object
|
|
34
|
-
* @param {Function} callback (value, key, object) => newValue // note if not changing value, should return value
|
|
35
|
-
* @returns {Object}
|
|
36
|
-
*/
|
|
37
|
-
export function mutateValues(object, callback) {
|
|
38
|
-
for (const key in object) {
|
|
39
|
-
object[key] = callback(object[key], key, object)
|
|
40
|
-
}
|
|
41
|
-
return object
|
|
42
|
-
}
|
|
43
|
-
|
|
44
31
|
// sorts undefined and null to the end if applicable
|
|
45
32
|
function compareUndefinedNull(a, b) {
|
|
46
33
|
if (b === undefined || b === null) {
|
|
@@ -109,30 +96,139 @@ export function multilevel(...comparators) {
|
|
|
109
96
|
}
|
|
110
97
|
}
|
|
111
98
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|
|
131
142
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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,7 +1,8 @@
|
|
|
1
1
|
/* eslint-disable no-restricted-syntax */
|
|
2
2
|
import { describe, expect, it, jest } from "@jest/globals"
|
|
3
|
+
import { findClosest } from "./array.js"
|
|
3
4
|
|
|
4
|
-
const { chunk, unique,
|
|
5
|
+
const { chunk, unique, ascending, descending, multilevel } = await import("./array.js")
|
|
5
6
|
|
|
6
7
|
describe("chunk", () => {
|
|
7
8
|
it("splits array into chunks of specified size", () => {
|
|
@@ -70,42 +71,6 @@ describe("unique", () => {
|
|
|
70
71
|
})
|
|
71
72
|
})
|
|
72
73
|
|
|
73
|
-
describe("mutateValues", () => {
|
|
74
|
-
it("mutates values in the object using the callback", () => {
|
|
75
|
-
const obj = { a: 1, b: 2 }
|
|
76
|
-
const result = mutateValues(obj, (v) => v * 2)
|
|
77
|
-
expect(result).toEqual({ a: 2, b: 4 })
|
|
78
|
-
expect(obj).toBe(result) // should mutate in place
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it("callback receives value, key, and object", () => {
|
|
82
|
-
const obj = { x: 1 }
|
|
83
|
-
const cb = jest.fn((v) => v + 1)
|
|
84
|
-
mutateValues(obj, cb)
|
|
85
|
-
expect(cb).toHaveBeenCalledWith(1, "x", obj)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it("returns the same object reference", () => {
|
|
89
|
-
const obj = { foo: "bar" }
|
|
90
|
-
const returned = mutateValues(obj, (v) => v)
|
|
91
|
-
expect(returned).toBe(obj)
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it("handles empty object", () => {
|
|
95
|
-
const obj = {}
|
|
96
|
-
expect(mutateValues(obj, (v) => v)).toEqual({})
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it("mutates inherited enumerable properties", () => {
|
|
100
|
-
const proto = { inherited: 1 }
|
|
101
|
-
const obj = Object.create(proto)
|
|
102
|
-
obj.own = 2
|
|
103
|
-
const result = mutateValues(obj, (v) => v + 1)
|
|
104
|
-
expect(result.own).toBe(3)
|
|
105
|
-
expect(result.inherited).toBe(2)
|
|
106
|
-
})
|
|
107
|
-
})
|
|
108
|
-
|
|
109
74
|
describe("ascending", () => {
|
|
110
75
|
it("sorts primitives ascending, undefined/null at end", () => {
|
|
111
76
|
const arr = [undefined, null, 3, 1, 2]
|
|
@@ -231,9 +196,18 @@ describe("multilevel", () => {
|
|
|
231
196
|
|
|
232
197
|
it("short-circuits after first non-zero comparator", () => {
|
|
233
198
|
const calls = []
|
|
234
|
-
const cmp1 = jest.fn(() => {
|
|
235
|
-
|
|
236
|
-
|
|
199
|
+
const cmp1 = jest.fn(() => {
|
|
200
|
+
calls.push("cmp1")
|
|
201
|
+
return 0
|
|
202
|
+
})
|
|
203
|
+
const cmp2 = jest.fn(() => {
|
|
204
|
+
calls.push("cmp2")
|
|
205
|
+
return -1
|
|
206
|
+
})
|
|
207
|
+
const cmp3 = jest.fn(() => {
|
|
208
|
+
calls.push("cmp3")
|
|
209
|
+
return 1
|
|
210
|
+
})
|
|
237
211
|
const cmp = multilevel(cmp1, cmp2, cmp3)
|
|
238
212
|
expect(cmp({}, {})).toBe(-1)
|
|
239
213
|
expect(calls).toEqual(["cmp1", "cmp2"])
|
|
@@ -246,28 +220,152 @@ describe("multilevel", () => {
|
|
|
246
220
|
})
|
|
247
221
|
})
|
|
248
222
|
|
|
249
|
-
describe("
|
|
250
|
-
it("returns
|
|
251
|
-
|
|
252
|
-
expect(
|
|
253
|
-
expect(
|
|
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()
|
|
254
253
|
})
|
|
255
254
|
|
|
256
|
-
it("returns
|
|
257
|
-
|
|
258
|
-
expect(
|
|
259
|
-
expect(
|
|
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()
|
|
260
259
|
})
|
|
261
260
|
|
|
262
|
-
it("
|
|
263
|
-
const
|
|
264
|
-
expect(
|
|
265
|
-
expect(
|
|
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
|
+
)
|
|
266
285
|
})
|
|
267
286
|
|
|
268
|
-
it("returns undefined if
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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()
|
|
272
370
|
})
|
|
273
371
|
})
|
package/src/index.js
CHANGED
package/src/math.js
CHANGED
|
@@ -9,6 +9,20 @@ export function mod(number, modulus) {
|
|
|
9
9
|
return ((number % modulus) + modulus) % modulus
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Given two points, returns a function
|
|
14
|
+
* This function, given an "x" value, returns a "y" value that is on the same line as the first two points.
|
|
15
|
+
* @param {[number, number]} firstPoint
|
|
16
|
+
* @param {[number, number]} secondPoint
|
|
17
|
+
* @returns {Function}
|
|
18
|
+
*/
|
|
19
|
+
export function line([x1, y1], [x2, y2]) {
|
|
20
|
+
const m = (y2 - y1) / (x2 - x1)
|
|
21
|
+
// m * x1 + b = y1
|
|
22
|
+
const b = y1 - m * x1
|
|
23
|
+
return (x) => m * x + b
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
/**
|
|
13
27
|
* Prepend a plus to a number or string if positive.
|
|
14
28
|
* @param {number|string} number Or string
|
package/src/math.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "@jest/globals"
|
|
2
|
-
const { mod, formatPlus } = await import("./math.js")
|
|
2
|
+
const { mod, formatPlus, line } = await import("./math.js")
|
|
3
3
|
|
|
4
4
|
describe("mod", () => {
|
|
5
5
|
it("returns n when n is less than m and n is non-negative", () => {
|
|
@@ -58,6 +58,52 @@ describe("mod", () => {
|
|
|
58
58
|
})
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
+
describe("line", () => {
|
|
62
|
+
it("returns a function that produces y for given x on the line through two points", () => {
|
|
63
|
+
const f = line([0, 0], [1, 1])
|
|
64
|
+
expect(f(0)).toBe(0)
|
|
65
|
+
expect(f(1)).toBe(1)
|
|
66
|
+
expect(f(2)).toBe(2)
|
|
67
|
+
expect(f(-1)).toBe(-1)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("handles non-origin, non-unit slope", () => {
|
|
71
|
+
const f = line([1, 2], [3, 6]) // slope 2, intercept 0
|
|
72
|
+
expect(f(1)).toBe(2)
|
|
73
|
+
expect(f(2)).toBe(4)
|
|
74
|
+
expect(f(3)).toBe(6)
|
|
75
|
+
expect(f(0)).toBe(0)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("handles negative slope", () => {
|
|
79
|
+
const f = line([0, 0], [2, -2]) // slope -1
|
|
80
|
+
expect(f(1)).toBe(-1)
|
|
81
|
+
expect(f(2)).toBe(-2)
|
|
82
|
+
expect(f(-2)).toBe(2)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("handles horizontal line", () => {
|
|
86
|
+
const f = line([1, 5], [3, 5]) // slope 0
|
|
87
|
+
expect(f(0)).toBe(5)
|
|
88
|
+
expect(f(100)).toBe(5)
|
|
89
|
+
expect(f(-100)).toBe(5)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("handles vertical line by returning NaN", () => {
|
|
93
|
+
const f = line([2, 1], [2, 5])
|
|
94
|
+
expect(f(2)).toBeNaN()
|
|
95
|
+
expect(f(3)).toBeNaN()
|
|
96
|
+
expect(f(1)).toBeNaN()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("works with negative coordinates", () => {
|
|
100
|
+
const f = line([-1, -2], [1, 2]) // slope 2
|
|
101
|
+
expect(f(-1)).toBe(-2)
|
|
102
|
+
expect(f(0)).toBe(0)
|
|
103
|
+
expect(f(1)).toBe(2)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
61
107
|
describe("formatPlus", () => {
|
|
62
108
|
it("prepends a plus for positive numbers", () => {
|
|
63
109
|
expect(formatPlus(1)).toBe("+1")
|
package/src/object.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutates the passed in object by calling callback on each of its values.
|
|
3
|
+
* @param {Object} object
|
|
4
|
+
* @param {Function} callback (value, key, object) => newValue // note if not changing value, should return value
|
|
5
|
+
* @returns {Object}
|
|
6
|
+
*/
|
|
7
|
+
export function mutateValues(object, callback) {
|
|
8
|
+
const keys = Object.keys(object)
|
|
9
|
+
for (const key of keys) {
|
|
10
|
+
object[key] = callback(object[key], key, object)
|
|
11
|
+
}
|
|
12
|
+
return object
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Mutates the passed object by removing any keys (`key in object`) that have a value of undefined.
|
|
17
|
+
* This is useful when the presence of a key alone causes something else to happen, but an undefined value is unexpected.
|
|
18
|
+
* In general, having objects with undefined values should not be encouraged but can happen as a byproduct of a code flow.
|
|
19
|
+
* @param {Object} object
|
|
20
|
+
* @returns {Object}
|
|
21
|
+
*/
|
|
22
|
+
export function deleteUndefinedValues(object) {
|
|
23
|
+
const keys = Object.keys(object)
|
|
24
|
+
for (const key of keys) {
|
|
25
|
+
if (key in object && object[key] === undefined) {
|
|
26
|
+
delete object[key]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return object
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates a function that accesses an object's value at key.
|
|
34
|
+
* @param {string} key
|
|
35
|
+
* @returns {Function}
|
|
36
|
+
*/
|
|
37
|
+
export function via(key) {
|
|
38
|
+
return (object) => object[key]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates a function that checks if the passed object contains the initial template.
|
|
43
|
+
* This means for each key in the template, the passed object has the same (===) value.
|
|
44
|
+
* @param {Object} template
|
|
45
|
+
* @returns {Function}
|
|
46
|
+
*/
|
|
47
|
+
export function contains(template) {
|
|
48
|
+
const keys = Object.keys(template)
|
|
49
|
+
return (object) => {
|
|
50
|
+
for (const key of keys) {
|
|
51
|
+
if (object[key] !== template[key]) {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* eslint-disable no-restricted-syntax */
|
|
2
|
+
import { jest } from "@jest/globals"
|
|
3
|
+
import { contains, deleteUndefinedValues, mutateValues, via } from "./object.js"
|
|
4
|
+
|
|
5
|
+
describe("mutateValues", () => {
|
|
6
|
+
it("mutates values in the object using the callback", () => {
|
|
7
|
+
const obj = { a: 1, b: 2 }
|
|
8
|
+
const result = mutateValues(obj, (v) => v * 2)
|
|
9
|
+
expect(result).toEqual({ a: 2, b: 4 })
|
|
10
|
+
expect(obj).toBe(result) // should mutate in place
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it("callback receives value, key, and object", () => {
|
|
14
|
+
const obj = { x: 1 }
|
|
15
|
+
const cb = jest.fn((v) => v + 1)
|
|
16
|
+
mutateValues(obj, cb)
|
|
17
|
+
expect(cb).toHaveBeenCalledWith(1, "x", obj)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("returns the same object reference", () => {
|
|
21
|
+
const obj = { foo: "bar" }
|
|
22
|
+
const returned = mutateValues(obj, (v) => v)
|
|
23
|
+
expect(returned).toBe(obj)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("handles empty object", () => {
|
|
27
|
+
const obj = {}
|
|
28
|
+
expect(mutateValues(obj, (v) => v)).toEqual({})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("does not mutate inherited enumerable properties", () => {
|
|
32
|
+
const proto = { inherited: 1 }
|
|
33
|
+
const obj = Object.create(proto)
|
|
34
|
+
obj.own = 2
|
|
35
|
+
const result = mutateValues(obj, (v) => v + 1)
|
|
36
|
+
expect(result.own).toBe(3)
|
|
37
|
+
expect(result.inherited).toBe(1)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe("deleteUndefinedValues", () => {
|
|
42
|
+
it("removes keys with undefined values", () => {
|
|
43
|
+
const obj = { a: 1, b: undefined, c: 3 }
|
|
44
|
+
const result = deleteUndefinedValues(obj)
|
|
45
|
+
expect(result).toEqual({ a: 1, c: 3 })
|
|
46
|
+
expect(obj).toBe(result)
|
|
47
|
+
expect("b" in result).toBe(false)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("does not remove keys with null or falsy non-undefined values", () => {
|
|
51
|
+
const obj = { a: null, b: 0, c: false, d: "", e: undefined }
|
|
52
|
+
deleteUndefinedValues(obj)
|
|
53
|
+
expect(obj).toEqual({ a: null, b: 0, c: false, d: "" })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("does nothing if no undefined values are present", () => {
|
|
57
|
+
const obj = { a: 1, b: 2 }
|
|
58
|
+
const result = deleteUndefinedValues(obj)
|
|
59
|
+
expect(result).toEqual({ a: 1, b: 2 })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("handles empty object", () => {
|
|
63
|
+
const obj = {}
|
|
64
|
+
expect(deleteUndefinedValues(obj)).toEqual({})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("removes own properties set to undefined but not inherited ones", () => {
|
|
68
|
+
const proto = { inherited: undefined }
|
|
69
|
+
const obj = Object.create(proto)
|
|
70
|
+
obj.own = undefined
|
|
71
|
+
deleteUndefinedValues(obj)
|
|
72
|
+
expect("own" in obj).toBe(false)
|
|
73
|
+
// inherited property remains accessible via prototype
|
|
74
|
+
expect(obj.inherited).toBeUndefined()
|
|
75
|
+
// but is not an own property
|
|
76
|
+
expect(Object.hasOwn(obj, "inherited")).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe("via", () => {
|
|
81
|
+
it("returns a function that accesses the given key", () => {
|
|
82
|
+
const getFoo = via("foo")
|
|
83
|
+
expect(getFoo({ foo: 42 })).toBe(42)
|
|
84
|
+
expect(getFoo({ foo: "bar" })).toBe("bar")
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("returns undefined if the key does not exist", () => {
|
|
88
|
+
const getX = via("x")
|
|
89
|
+
expect(getX({})).toBeUndefined()
|
|
90
|
+
expect(getX({ y: 1 })).toBeUndefined()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("works with numeric keys", () => {
|
|
94
|
+
const get0 = via(0)
|
|
95
|
+
expect(get0([10, 20])).toBe(10)
|
|
96
|
+
expect(get0({ 0: "zero" })).toBe("zero")
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("returns undefined if object is missing", () => {
|
|
100
|
+
const getFoo = via("foo")
|
|
101
|
+
expect(() => getFoo(undefined)).toThrow(TypeError)
|
|
102
|
+
expect(() => getFoo(null)).toThrow(TypeError)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe("contains", () => {
|
|
107
|
+
it("returns true when object contains all template keys with same values", () => {
|
|
108
|
+
const template = { a: 1, b: 2 }
|
|
109
|
+
const fn = contains(template)
|
|
110
|
+
expect(fn({ a: 1, b: 2, c: 3 })).toBe(true)
|
|
111
|
+
expect(fn({ a: 1, b: 2 })).toBe(true)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("returns false if any template key is missing", () => {
|
|
115
|
+
const template = { a: 1, b: 2 }
|
|
116
|
+
const fn = contains(template)
|
|
117
|
+
expect(fn({ a: 1 })).toBe(false)
|
|
118
|
+
expect(fn({ b: 2 })).toBe(false)
|
|
119
|
+
expect(fn({})).toBe(false)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("returns false if any template key has a different value", () => {
|
|
123
|
+
const template = { a: 1, b: 2 }
|
|
124
|
+
const fn = contains(template)
|
|
125
|
+
expect(fn({ a: 1, b: 3 })).toBe(false)
|
|
126
|
+
expect(fn({ a: 2, b: 2 })).toBe(false)
|
|
127
|
+
expect(fn({ a: 2, b: 3 })).toBe(false)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it("works with empty template (always true)", () => {
|
|
131
|
+
const fn = contains({})
|
|
132
|
+
expect(fn({})).toBe(true)
|
|
133
|
+
expect(fn({ a: 1 })).toBe(true)
|
|
134
|
+
expect(fn({ a: undefined })).toBe(true)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("does not require object to have only template keys", () => {
|
|
138
|
+
const template = { x: 5 }
|
|
139
|
+
const fn = contains(template)
|
|
140
|
+
expect(fn({ x: 5, y: 10 })).toBe(true)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("uses strict equality (===) for comparison", () => {
|
|
144
|
+
const template = { a: 0 }
|
|
145
|
+
const fn = contains(template)
|
|
146
|
+
expect(fn({ a: false })).toBe(false)
|
|
147
|
+
expect(fn({ a: "0" })).toBe(false)
|
|
148
|
+
expect(fn({ a: 0 })).toBe(true)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// ISSUE: contains() does not check for own properties only; it will match inherited properties.
|
|
152
|
+
it("matches inherited properties in the object", () => {
|
|
153
|
+
const template = { foo: 1 }
|
|
154
|
+
const fn = contains(template)
|
|
155
|
+
const proto = { foo: 1 }
|
|
156
|
+
const obj = Object.create(proto)
|
|
157
|
+
expect(fn(obj)).toBe(true)
|
|
158
|
+
obj.foo = 2
|
|
159
|
+
expect(fn(obj)).toBe(false)
|
|
160
|
+
})
|
|
161
|
+
})
|