@tim-code/my-util 0.5.9 → 0.5.12
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/math.js +33 -1
- package/src/math.test.js +128 -3
- package/src/promise.js +2 -2
- package/src/promise.test.js +9 -0
package/package.json
CHANGED
package/src/math.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ascending } from "./array.js"
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Gives the remainder when the number is divided by modulus.
|
|
3
5
|
* The sign of the remainder is always the same as the modulus i.e. mod(-1, 3) === 2 (not -1 as in -1 % 3).
|
|
@@ -128,7 +130,7 @@ export function formatPlus(number, { zero = false } = {}) {
|
|
|
128
130
|
* Create an array of numbers progressing from start up to, but not including, end.
|
|
129
131
|
* @param {number} start
|
|
130
132
|
* @param {number=} end
|
|
131
|
-
* @param {number=}
|
|
133
|
+
* @param {number=} increment
|
|
132
134
|
* @returns {number[]}
|
|
133
135
|
*/
|
|
134
136
|
export function range(start, end, increment = 1) {
|
|
@@ -150,3 +152,33 @@ export function range(start, end, increment = 1) {
|
|
|
150
152
|
export function isNumber(number) {
|
|
151
153
|
return Number.isFinite(number)
|
|
152
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Returns an object mapping quantile labels ("0", "10", ..., "100" for N = 10)
|
|
158
|
+
* to the corresponding element in the array at that percentile.
|
|
159
|
+
* @template T
|
|
160
|
+
* @param {T[]} array
|
|
161
|
+
* @param {Object} $1
|
|
162
|
+
* @param {number} $1.N How many quantiles
|
|
163
|
+
* @param {string|number|Function=} $1.key Can specify a key of the object to sort on or a function.
|
|
164
|
+
* @param {Function=} $1.method Method to use to choose which element when the percentile index is a fractional value.
|
|
165
|
+
* Default is Math.round.
|
|
166
|
+
* @returns {Object|undefined} Returns undefined is array is empty
|
|
167
|
+
*/
|
|
168
|
+
export function quantiles(array, { N, key, method = Math.round }) {
|
|
169
|
+
if (!(N > 0) || N % 1 !== 0) {
|
|
170
|
+
throw new Error("N must be a positive integer")
|
|
171
|
+
}
|
|
172
|
+
if (!array.length) {
|
|
173
|
+
return undefined
|
|
174
|
+
}
|
|
175
|
+
const sorted = [...array].sort(ascending(key))
|
|
176
|
+
const result = {}
|
|
177
|
+
for (let i = 0; i <= N; i++) {
|
|
178
|
+
const percentile = i / N
|
|
179
|
+
const percentileIndex = method(percentile * (sorted.length - 1))
|
|
180
|
+
const label = method(i * (100 / N))
|
|
181
|
+
result[label] = sorted[percentileIndex]
|
|
182
|
+
}
|
|
183
|
+
return result
|
|
184
|
+
}
|
package/src/math.test.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { describe, expect, it } from "@jest/globals"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
|
|
3
|
+
// EXPORTED FUNCTIONS UNDER TEST:
|
|
4
|
+
// - mod
|
|
5
|
+
// - line
|
|
6
|
+
// - sum
|
|
7
|
+
// - average
|
|
8
|
+
// - variance
|
|
9
|
+
// - formatPlus
|
|
10
|
+
// - range
|
|
11
|
+
// - isNumber
|
|
12
|
+
// - quantiles
|
|
13
|
+
const { mod, formatPlus, line, sum, average, variance, range, isNumber, quantiles } =
|
|
14
|
+
await import("./math.js")
|
|
5
15
|
|
|
6
16
|
describe("mod", () => {
|
|
7
17
|
it("returns n when n is less than m and n is non-negative", () => {
|
|
@@ -98,6 +108,7 @@ describe("line", () => {
|
|
|
98
108
|
expect(f(1)).toBeNaN()
|
|
99
109
|
})
|
|
100
110
|
|
|
111
|
+
// ISSUE: line() does not guard against identical points (x1===x2 and y1===y2), which yields NaN for all x. Consider throwing or documenting behavior for degenerate input.
|
|
101
112
|
it("works with negative coordinates", () => {
|
|
102
113
|
const f = line([-1, -2], [1, 2]) // slope 2
|
|
103
114
|
expect(f(-1)).toBe(-2)
|
|
@@ -255,6 +266,8 @@ describe("formatPlus", () => {
|
|
|
255
266
|
expect(formatPlus(Symbol("x"))).toBeUndefined()
|
|
256
267
|
expect(formatPlus(NaN)).toBeUndefined()
|
|
257
268
|
})
|
|
269
|
+
|
|
270
|
+
// ISSUE: formatPlus() treats any string not starting with "-" as positive, e.g. "+5" becomes "++5" and "abc" becomes "+abc". Consider handling leading "+" or non-numeric strings explicitly.
|
|
258
271
|
})
|
|
259
272
|
|
|
260
273
|
describe("range", () => {
|
|
@@ -330,3 +343,115 @@ describe("isNumber", () => {
|
|
|
330
343
|
expect(isNumber(() => 1)).toBe(false)
|
|
331
344
|
})
|
|
332
345
|
})
|
|
346
|
+
|
|
347
|
+
describe("quantiles", () => {
|
|
348
|
+
it("maps 0..100 deciles for an already sorted array of length 11 (default rounding)", () => {
|
|
349
|
+
const arr = Array.from({ length: 11 }, (_, i) => i)
|
|
350
|
+
const result = quantiles(arr, { N: 10 })
|
|
351
|
+
for (let i = 0; i <= 10; i++) {
|
|
352
|
+
expect(result[i * 10]).toBe(i)
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it("sorts the input before selecting percentiles", () => {
|
|
357
|
+
const arr = [9, 7, 5, 3, 1, 2, 4, 6, 8, 0, 10]
|
|
358
|
+
const result = quantiles(arr, { N: 10 })
|
|
359
|
+
for (let i = 0; i <= 10; i++) {
|
|
360
|
+
expect(result[i * 10]).toBe(i)
|
|
361
|
+
}
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it("supports a string key to select values for percentile positions", () => {
|
|
365
|
+
const arr = [
|
|
366
|
+
{ v: 9 },
|
|
367
|
+
{ v: 7 },
|
|
368
|
+
{ v: 5 },
|
|
369
|
+
{ v: 3 },
|
|
370
|
+
{ v: 1 },
|
|
371
|
+
{ v: 2 },
|
|
372
|
+
{ v: 4 },
|
|
373
|
+
{ v: 6 },
|
|
374
|
+
{ v: 8 },
|
|
375
|
+
{ v: 0 },
|
|
376
|
+
{ v: 10 },
|
|
377
|
+
]
|
|
378
|
+
const result = quantiles(arr, { N: 10, key: "v" })
|
|
379
|
+
for (let i = 0; i <= 10; i++) {
|
|
380
|
+
expect(result[i * 10].v).toBe(i)
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it("supports a key function", () => {
|
|
385
|
+
const arr = [
|
|
386
|
+
{ n: 90 },
|
|
387
|
+
{ n: 70 },
|
|
388
|
+
{ n: 50 },
|
|
389
|
+
{ n: 30 },
|
|
390
|
+
{ n: 10 },
|
|
391
|
+
{ n: 20 },
|
|
392
|
+
{ n: 40 },
|
|
393
|
+
{ n: 60 },
|
|
394
|
+
{ n: 80 },
|
|
395
|
+
{ n: 0 },
|
|
396
|
+
{ n: 100 },
|
|
397
|
+
]
|
|
398
|
+
const result = quantiles(arr, { N: 10, key: (el) => el.n })
|
|
399
|
+
for (let i = 0; i <= 10; i++) {
|
|
400
|
+
expect(result[i * 10].n).toBe(i * 10)
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it("respects a custom method (Math.floor) for fractional indices (N=10)", () => {
|
|
405
|
+
const arr = Array.from({ length: 10 }, (_, i) => i) // 0..9
|
|
406
|
+
const defaultResult = quantiles(arr, { N: 10 }) // uses Math.round
|
|
407
|
+
const floorResult = quantiles(arr, { N: 10, method: Math.floor })
|
|
408
|
+
expect(defaultResult[50]).toBe(5) // round(0.5 * 9) = 5
|
|
409
|
+
expect(floorResult[50]).toBe(4) // floor(0.5 * 9) = 4
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it("handles arrays of length 1 by returning that element for all deciles", () => {
|
|
413
|
+
const arr = [42]
|
|
414
|
+
const result = quantiles(arr, { N: 10 })
|
|
415
|
+
for (let i = 0; i <= 10; i++) {
|
|
416
|
+
expect(result[i * 10]).toBe(42)
|
|
417
|
+
}
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it("supports quartiles (N=4) with expected labels 0,25,50,75,100", () => {
|
|
421
|
+
const arr = Array.from({ length: 9 }, (_, i) => i) // 0..8
|
|
422
|
+
const result = quantiles(arr, { N: 4 })
|
|
423
|
+
expect(result[0]).toBe(0) // index round(0 * 8) = 0
|
|
424
|
+
expect(result[25]).toBe(2) // index round(0.25* 8) = 2
|
|
425
|
+
expect(result[50]).toBe(4) // index round(0.5 * 8) = 4
|
|
426
|
+
expect(result[75]).toBe(6) // index round(0.75* 8) = 6
|
|
427
|
+
expect(result[100]).toBe(8) // index round(1 * 8) = 8
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it("supports tertiles (N=3) and label rounding to 33 and 67", () => {
|
|
431
|
+
const arr = Array.from({ length: 11 }, (_, i) => i) // 0..10
|
|
432
|
+
const result = quantiles(arr, { N: 3 }) // labels via round(i*(100/3)) => 0,33,67,100
|
|
433
|
+
expect(result[0]).toBe(0) // round(0/3 * 10) = 0
|
|
434
|
+
expect(result[33]).toBe(3) // round(1/3 * 10) = 3
|
|
435
|
+
expect(result[67]).toBe(7) // round(2/3 * 10) = 7
|
|
436
|
+
expect(result[100]).toBe(10) // round(1 * 10) = 10
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it("respects a custom method (Math.floor) for fractional indices when N != 10", () => {
|
|
440
|
+
const arr = Array.from({ length: 10 }, (_, i) => i) // 0..9
|
|
441
|
+
const defaultResult = quantiles(arr, { N: 4 }) // uses Math.round
|
|
442
|
+
const floorResult = quantiles(arr, { N: 4, method: Math.floor })
|
|
443
|
+
expect(defaultResult[50]).toBe(5) // round(0.5 * 9) = 5
|
|
444
|
+
expect(floorResult[50]).toBe(4) // floor(0.5 * 9) = 4
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it("returns undefined for empty array", () => {
|
|
448
|
+
expect(quantiles([], { N: 4 })).toBeUndefined()
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it("throws if N is negative or not an integer", () => {
|
|
452
|
+
expect(() => quantiles([1, 2, 3], {})).toThrow("N must be a positive integer")
|
|
453
|
+
expect(() => quantiles([1, 2, 3], { N: -1 })).toThrow("N must be a positive integer")
|
|
454
|
+
expect(() => quantiles([1, 2, 3], { N: 0 })).toThrow("N must be a positive integer")
|
|
455
|
+
expect(() => quantiles([1, 2, 3], { N: 2.5 })).toThrow("N must be a positive integer")
|
|
456
|
+
})
|
|
457
|
+
})
|
package/src/promise.js
CHANGED
|
@@ -72,12 +72,12 @@ export async function sleep(ms) {
|
|
|
72
72
|
* @param {boolean=} $1.flatten Flattens values before returning; useful if promises return arrays
|
|
73
73
|
* @param {boolean=} $1.abort If true, will return early if there are errors.
|
|
74
74
|
* If false (default), will process all elements in the array (like Promise.allSettled()).
|
|
75
|
-
* @param {Function} callback
|
|
75
|
+
* @param {Function} callback Default is identity function to enable passing promises as "array".
|
|
76
76
|
* @returns {Object} {results, values, returned, errors}
|
|
77
77
|
*/
|
|
78
78
|
export async function allSettled(
|
|
79
79
|
{ array, limit, limiter, flatten = false, abort = false },
|
|
80
|
-
callback
|
|
80
|
+
callback = (promise) => promise
|
|
81
81
|
) {
|
|
82
82
|
const results = []
|
|
83
83
|
let returned = []
|
package/src/promise.test.js
CHANGED
|
@@ -132,6 +132,15 @@ describe("allSettled", () => {
|
|
|
132
132
|
expect(result.results.every((r) => r.status === "fulfilled")).toBe(true)
|
|
133
133
|
})
|
|
134
134
|
|
|
135
|
+
it("returns correct structure for all fulfilled", async () => {
|
|
136
|
+
const arr = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]
|
|
137
|
+
const result = await allSettled({ array: arr })
|
|
138
|
+
expect(result.values).toEqual([1, 2, 3])
|
|
139
|
+
expect(result.returned).toEqual([1, 2, 3])
|
|
140
|
+
expect(result.errors).toEqual([])
|
|
141
|
+
expect(result.results.every((r) => r.status === "fulfilled")).toBe(true)
|
|
142
|
+
})
|
|
143
|
+
|
|
135
144
|
it("handles rejected promises and collects errors", async () => {
|
|
136
145
|
const arr = [1, 2, 3]
|
|
137
146
|
const cb = (x) => (x === 2 ? Promise.reject("fail") : x + 1)
|