@tim-code/my-util 0.5.8 → 0.5.11
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 +2 -2
- package/src/array.js +30 -15
- package/src/array.test.js +83 -5
- package/src/find.test.js +0 -1
- package/src/math.js +33 -1
- package/src/math.test.js +128 -5
- package/src/object.test.js +0 -1
- package/src/promise.test.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tim-code/my-util",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Tim Sprowl",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@jest/globals": "^29.7.0",
|
|
32
|
-
"@tim-code/eslint-config": "^1.
|
|
32
|
+
"@tim-code/eslint-config": "^1.4.0",
|
|
33
33
|
"jest": "^29.7.0"
|
|
34
34
|
},
|
|
35
35
|
"jest": {
|
package/src/array.js
CHANGED
|
@@ -186,17 +186,16 @@ export function multilevel(...comparators) {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
function siftDown(heap, i, compare) {
|
|
190
|
-
const n = heap.length
|
|
189
|
+
function siftDown(heap, i, compare, length) {
|
|
191
190
|
// eslint-disable-next-line no-constant-condition
|
|
192
191
|
while (true) {
|
|
193
192
|
const left = 2 * i + 1
|
|
194
193
|
const right = left + 1
|
|
195
194
|
let largest = i
|
|
196
|
-
if (left <
|
|
195
|
+
if (left < length && compare(heap[left], heap[largest]) > 0) {
|
|
197
196
|
largest = left
|
|
198
197
|
}
|
|
199
|
-
if (right <
|
|
198
|
+
if (right < length && compare(heap[right], heap[largest]) > 0) {
|
|
200
199
|
largest = right
|
|
201
200
|
}
|
|
202
201
|
if (largest === i) {
|
|
@@ -209,11 +208,11 @@ function siftDown(heap, i, compare) {
|
|
|
209
208
|
}
|
|
210
209
|
}
|
|
211
210
|
|
|
212
|
-
function maxHeapify(heap, compare) {
|
|
211
|
+
function maxHeapify(heap, compare, length) {
|
|
213
212
|
// (heap.length >>> 1) is equivalent to Math.floor(heap.length / 2)
|
|
214
213
|
// eslint-disable-next-line no-bitwise
|
|
215
|
-
for (let i = (
|
|
216
|
-
siftDown(heap, i, compare)
|
|
214
|
+
for (let i = (length >>> 1) - 1; i >= 0; i--) {
|
|
215
|
+
siftDown(heap, i, compare, length)
|
|
217
216
|
}
|
|
218
217
|
}
|
|
219
218
|
|
|
@@ -222,34 +221,50 @@ function maxHeapify(heap, compare) {
|
|
|
222
221
|
* @template T
|
|
223
222
|
* @param {Array<T>} array
|
|
224
223
|
* @param {Object} $1
|
|
225
|
-
* @param {number} $1.N Number of elements to return
|
|
224
|
+
* @param {number} $1.N Number of elements to return; must be a nonnegative integer
|
|
226
225
|
* @param {Function=} $1.compare Sort function. Default is ascending sort.
|
|
227
226
|
* @param {boolean=} $1.unsorted If true, returns the final result in heap order, not sorted order, as an optimization.
|
|
228
227
|
* Default is false.
|
|
229
228
|
* @param {boolean=} $1.force If true, will force heap-based method for small N instead of more efficient "normal" way.
|
|
230
229
|
* Default is false.
|
|
230
|
+
* @param {boolean=} $1.mutate If true, will mutate the original array rather than copy it.
|
|
231
231
|
* @returns {Array<T>}
|
|
232
232
|
*/
|
|
233
|
-
export function sortN(
|
|
234
|
-
|
|
233
|
+
export function sortN(
|
|
234
|
+
array,
|
|
235
|
+
{ N, compare = ascending(), unsorted = false, force = false, mutate = false }
|
|
236
|
+
) {
|
|
237
|
+
if (!(N >= 0) || N % 1 !== 0) {
|
|
238
|
+
throw new Error("N must be a nonnegative integer")
|
|
239
|
+
}
|
|
240
|
+
if (N === 0) {
|
|
241
|
+
if (mutate) {
|
|
242
|
+
array.length = 0
|
|
243
|
+
return array
|
|
244
|
+
}
|
|
235
245
|
return []
|
|
236
246
|
}
|
|
237
247
|
if (N >= array.length) {
|
|
238
|
-
return [...array].sort(compare)
|
|
248
|
+
return (mutate ? array : [...array]).sort(compare)
|
|
239
249
|
}
|
|
240
250
|
if (!force && array.length <= 100 && N / array.length >= 0.1) {
|
|
241
251
|
// seems to be faster to do it the "normal" way in this case
|
|
242
|
-
|
|
252
|
+
const sorted = (mutate ? array : [...array]).sort(compare)
|
|
253
|
+
sorted.length = N
|
|
254
|
+
return sorted
|
|
243
255
|
}
|
|
244
|
-
const heap = array.slice(0, N)
|
|
245
|
-
maxHeapify(heap, compare)
|
|
256
|
+
const heap = mutate ? array : array.slice(0, N)
|
|
257
|
+
maxHeapify(heap, compare, N)
|
|
246
258
|
for (let i = N; i < array.length; i++) {
|
|
247
259
|
const element = array[i]
|
|
248
260
|
if (compare(element, heap[0]) < 0) {
|
|
249
261
|
heap[0] = element
|
|
250
|
-
siftDown(heap, 0, compare)
|
|
262
|
+
siftDown(heap, 0, compare, N)
|
|
251
263
|
}
|
|
252
264
|
}
|
|
265
|
+
if (mutate) {
|
|
266
|
+
heap.length = N
|
|
267
|
+
}
|
|
253
268
|
if (unsorted) {
|
|
254
269
|
return heap
|
|
255
270
|
}
|
package/src/array.test.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-restricted-syntax */
|
|
2
1
|
import { describe, expect, it, jest } from "@jest/globals"
|
|
3
2
|
|
|
4
3
|
const { chunk, unique, duplicates, ascending, descending, multilevel, sortN } = await import(
|
|
@@ -515,7 +514,12 @@ describe("multilevel", () => {
|
|
|
515
514
|
describe("sortN", () => {
|
|
516
515
|
it("returns empty array when N <= 0", () => {
|
|
517
516
|
expect(sortN([1, 2, 3], { N: 0, force: true })).toEqual([])
|
|
518
|
-
expect(sortN([1, 2, 3], { N: -5, force: true })).
|
|
517
|
+
expect(() => sortN([1, 2, 3], { N: -5, force: true })).toThrow(
|
|
518
|
+
/N must be a nonnegative integer/u
|
|
519
|
+
)
|
|
520
|
+
expect(() => sortN([1, 2, 3], { N: 1.5, force: true })).toThrow(
|
|
521
|
+
/N must be a nonnegative integer/u
|
|
522
|
+
)
|
|
519
523
|
})
|
|
520
524
|
|
|
521
525
|
it("returns the entire array sorted when N >= array.length and does not mutate original", () => {
|
|
@@ -526,7 +530,7 @@ describe("sortN", () => {
|
|
|
526
530
|
expect(result).not.toBe(arr)
|
|
527
531
|
})
|
|
528
532
|
|
|
529
|
-
it("returns the first N smallest elements (default ascending comparator)", () => {
|
|
533
|
+
it("returns the first N smallest elements (default ascending comparator, heap path)", () => {
|
|
530
534
|
expect(sortN([5, 1, 3, 2, 4], { N: 3, force: true })).toEqual([1, 2, 3])
|
|
531
535
|
expect(sortN([3, 1, 3, 2, 2], { N: 4, force: true })).toEqual([1, 2, 2, 3])
|
|
532
536
|
})
|
|
@@ -542,14 +546,88 @@ describe("sortN", () => {
|
|
|
542
546
|
expect(out.map((o) => o.v)).toEqual([1, 2, 3])
|
|
543
547
|
})
|
|
544
548
|
|
|
545
|
-
it("does not mutate the original array when N < array.length", () => {
|
|
549
|
+
it("does not mutate the original array when N < array.length (heap path)", () => {
|
|
546
550
|
const arr = [5, 1, 3, 2, 4]
|
|
547
551
|
const out = sortN(arr, { N: 3, force: true })
|
|
548
552
|
expect(out).toEqual([1, 2, 3])
|
|
549
553
|
expect(arr).toEqual([5, 1, 3, 2, 4])
|
|
550
554
|
})
|
|
551
555
|
|
|
552
|
-
it("returns the
|
|
556
|
+
it("returns the N elements in heap order when unsorted=true (heap path)", () => {
|
|
553
557
|
expect(sortN([10, 1, 7, 3, 5], { N: 3, force: true, unsorted: true })).toEqual([5, 1, 3])
|
|
554
558
|
})
|
|
559
|
+
|
|
560
|
+
it("uses the 'normal' sort+truncate path when force=false and does not mutate original", () => {
|
|
561
|
+
const arr = [4, 2, 5, 1, 3] // small array and N/len >= 0.1 -> normal path
|
|
562
|
+
const out = sortN(arr, { N: 2 })
|
|
563
|
+
expect(out).toEqual([1, 2])
|
|
564
|
+
expect(arr).toEqual([4, 2, 5, 1, 3])
|
|
565
|
+
expect(out).not.toBe(arr)
|
|
566
|
+
})
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
describe("sortN mutate", () => {
|
|
570
|
+
it("returns empty array when N <= 0", () => {
|
|
571
|
+
const a = [1, 2, 3]
|
|
572
|
+
const result = sortN(a, { N: 0, force: true, mutate: true })
|
|
573
|
+
expect(a).toEqual([])
|
|
574
|
+
expect(result).toBe(a)
|
|
575
|
+
const b = [1, 2, 3]
|
|
576
|
+
expect(() => sortN(b, { N: -5, force: true, mutate: true })).toThrow(
|
|
577
|
+
/N must be a nonnegative integer/u
|
|
578
|
+
)
|
|
579
|
+
expect(() => sortN(b, { N: -1.5, force: true, mutate: true })).toThrow(
|
|
580
|
+
/N must be a nonnegative integer/u
|
|
581
|
+
)
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
it("returns the entire array sorted when N >= array.length", () => {
|
|
585
|
+
const array = [3, 1, 2]
|
|
586
|
+
const result = sortN(array, { N: 10, force: true, mutate: true })
|
|
587
|
+
expect(array).toEqual([1, 2, 3])
|
|
588
|
+
expect(result).toBe(array)
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
it("returns the first N smallest elements and mutates in-place (heap path)", () => {
|
|
592
|
+
const arr = [5, 1, 3, 2, 4]
|
|
593
|
+
const out = sortN(arr, { N: 3, force: true, mutate: true })
|
|
594
|
+
expect(out).toBe(arr)
|
|
595
|
+
expect(arr).toEqual([1, 2, 3])
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
it("respects a descending comparator and mutates in-place (heap path)", () => {
|
|
599
|
+
const arr = [5, 1, 3, 2, 4]
|
|
600
|
+
const out = sortN(arr, { N: 2, compare: descending(), force: true, mutate: true })
|
|
601
|
+
expect(out).toBe(arr)
|
|
602
|
+
expect(arr).toEqual([5, 4])
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
it("works with key-based comparator, mutating and truncating in-place (heap path)", () => {
|
|
606
|
+
const arr = [{ v: 3 }, {}, { v: 1 }, { v: null }, { v: 2 }, { v: undefined }]
|
|
607
|
+
const out = sortN(arr, { N: 3, compare: ascending("v"), force: true, mutate: true })
|
|
608
|
+
expect(out).toBe(arr)
|
|
609
|
+
expect(arr.length).toBe(3)
|
|
610
|
+
expect(out.map((o) => o.v)).toEqual([1, 2, 3])
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
it("mutates the original array when mutate=true and N < array.length (heap path)", () => {
|
|
614
|
+
const arr = [5, 1, 3, 2, 4]
|
|
615
|
+
const out = sortN(arr, { N: 3, force: true, mutate: true })
|
|
616
|
+
expect(out).toBe(arr)
|
|
617
|
+
expect(arr).toEqual([1, 2, 3])
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
it("returns the first N elements in heap order when unsorted=true (heap path, mutate=true)", () => {
|
|
621
|
+
const arr = [10, 1, 7, 3, 5]
|
|
622
|
+
const out = sortN(arr, { N: 3, force: true, unsorted: true, mutate: true })
|
|
623
|
+
expect(out).toBe(arr)
|
|
624
|
+
expect(arr).toEqual([5, 1, 3])
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
it("uses the 'normal' sort+truncate path when force=false and mutates in-place", () => {
|
|
628
|
+
const arr = [4, 2, 5, 1, 3] // small array and N/len >= 0.1 -> normal path
|
|
629
|
+
const out = sortN(arr, { N: 2, mutate: true })
|
|
630
|
+
expect(out).toBe(arr)
|
|
631
|
+
expect(arr).toEqual([1, 2])
|
|
632
|
+
})
|
|
555
633
|
})
|
package/src/find.test.js
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)
|
|
@@ -248,7 +259,6 @@ describe("formatPlus", () => {
|
|
|
248
259
|
|
|
249
260
|
it("returns undefined for non-number, non-string input", () => {
|
|
250
261
|
expect(formatPlus(undefined)).toBeUndefined()
|
|
251
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
252
262
|
expect(formatPlus(null)).toBeUndefined()
|
|
253
263
|
expect(formatPlus({})).toBeUndefined()
|
|
254
264
|
expect(formatPlus([])).toBeUndefined()
|
|
@@ -256,6 +266,8 @@ describe("formatPlus", () => {
|
|
|
256
266
|
expect(formatPlus(Symbol("x"))).toBeUndefined()
|
|
257
267
|
expect(formatPlus(NaN)).toBeUndefined()
|
|
258
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.
|
|
259
271
|
})
|
|
260
272
|
|
|
261
273
|
describe("range", () => {
|
|
@@ -323,7 +335,6 @@ describe("isNumber", () => {
|
|
|
323
335
|
it("returns false for non-number types", () => {
|
|
324
336
|
expect(isNumber("123")).toBe(false)
|
|
325
337
|
expect(isNumber(undefined)).toBe(false)
|
|
326
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
327
338
|
expect(isNumber(null)).toBe(false)
|
|
328
339
|
expect(isNumber({})).toBe(false)
|
|
329
340
|
expect(isNumber([])).toBe(false)
|
|
@@ -332,3 +343,115 @@ describe("isNumber", () => {
|
|
|
332
343
|
expect(isNumber(() => 1)).toBe(false)
|
|
333
344
|
})
|
|
334
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/object.test.js
CHANGED
package/src/promise.test.js
CHANGED