@cloudglides/nox 1.1.6 → 3.0.0

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 (66) hide show
  1. package/README.md +9 -9
  2. package/example/package.json +1 -1
  3. package/example/src/App.css +38 -7
  4. package/example/src/App.jsx +328 -25
  5. package/package.json +7 -6
  6. package/pnpm-workspace.yaml +3 -0
  7. package/src/core.browser.js +37 -4
  8. package/src/core.js +37 -4
  9. package/src/generators/logistic.js +30 -25
  10. package/src/generators/mixer.js +7 -7
  11. package/src/generators/mt19937.js +10 -7
  12. package/src/generators/pcg64.js +23 -12
  13. package/src/generators/splitmix64.js +12 -6
  14. package/src/generators/tent.js +12 -7
  15. package/src/generators/xorshift64.js +6 -3
  16. package/src/index.browser.js +168 -1
  17. package/src/index.d.ts +68 -4
  18. package/src/index.js +169 -1
  19. package/src/rng.browser.js +5 -6
  20. package/src/rng.js +95 -94
  21. package/src/utils/arrays.js +149 -0
  22. package/src/utils/bits.js +146 -21
  23. package/src/utils/categorical.js +68 -31
  24. package/src/utils/clustering.js +143 -0
  25. package/src/utils/combinatorics.js +113 -69
  26. package/src/utils/confidence.js +145 -0
  27. package/src/utils/decomposition.js +204 -0
  28. package/src/utils/diagnostics.js +309 -0
  29. package/src/utils/distributions-advanced.js +122 -0
  30. package/src/utils/distributions-extra.js +102 -11
  31. package/src/utils/distributions-special.js +77 -20
  32. package/src/utils/distributions.js +99 -35
  33. package/src/utils/effects.js +172 -0
  34. package/src/utils/entropy.browser.js +29 -26
  35. package/src/utils/entropy.js +18 -8
  36. package/src/utils/helpers.js +64 -0
  37. package/src/utils/hypothesis.js +167 -0
  38. package/src/utils/integration.js +137 -0
  39. package/src/utils/interpolation.js +221 -0
  40. package/src/utils/logistic.js +91 -0
  41. package/src/utils/matrix.js +242 -0
  42. package/src/utils/noise.js +36 -22
  43. package/src/utils/odesolvers.js +176 -0
  44. package/src/utils/optimization.js +215 -0
  45. package/src/utils/polynomial.js +136 -0
  46. package/src/utils/precomputed.js +166 -0
  47. package/src/utils/probability.js +199 -0
  48. package/src/utils/pvalues.js +142 -0
  49. package/src/utils/regression.js +170 -0
  50. package/src/utils/resampling.js +112 -0
  51. package/src/utils/rootfinding.js +158 -0
  52. package/src/utils/sampling.js +86 -77
  53. package/src/utils/seed.js +10 -4
  54. package/src/utils/seeding.js +24 -12
  55. package/src/utils/sequence.js +116 -32
  56. package/src/utils/state.js +48 -36
  57. package/src/utils/statistics.js +64 -2
  58. package/src/utils/stochastic.js +91 -31
  59. package/src/utils/stratified.js +108 -0
  60. package/src/utils/timeseries.js +166 -0
  61. package/src/utils/transforms.js +146 -0
  62. package/test/comprehensive.js +4 -3
  63. package/test/new-features.js +52 -0
  64. package/IMPROVEMENTS.md +0 -58
  65. package/PERFORMANCE.md +0 -69
  66. package/example/pnpm-lock.yaml +0 -1006
@@ -1,88 +1,97 @@
1
1
  export const weightedPick = (arr, weights, rng) => {
2
- if (!Array.isArray(arr) || arr.length === 0) {
3
- throw new TypeError('arr must be a non-empty array');
4
- }
5
- if (!Array.isArray(weights) || weights.length === 0) {
6
- throw new TypeError('weights must be a non-empty array');
7
- }
8
- if (arr.length !== weights.length) {
9
- throw new Error('arr and weights must have same length');
10
- }
11
- if (!rng || typeof rng.nextFloat !== 'function') {
12
- throw new TypeError('rng must be an RNG instance');
13
- }
14
-
15
- const total = weights.reduce((a, b) => {
16
- if (typeof a !== 'number' || typeof b !== 'number') {
17
- throw new TypeError('All weights must be numbers');
2
+ if (!Array.isArray(arr) || arr.length === 0) {
3
+ throw new RangeError('arr must be a non-empty array');
4
+ }
5
+ if (!Array.isArray(weights) || weights.length === 0) {
6
+ throw new RangeError('weights must be a non-empty array');
7
+ }
8
+ if (arr.length !== weights.length) {
9
+ throw new RangeError('arr and weights must have same length');
10
+ }
11
+ if (!rng || typeof rng.nextFloat !== 'function') {
12
+ throw new TypeError('rng must be an RNG instance');
13
+ }
14
+
15
+ let total = 0;
16
+ for (let i = 0; i < weights.length; i++) {
17
+ if (typeof weights[i] !== 'number') {
18
+ throw new TypeError('All weights must be numbers');
19
+ }
20
+ total += weights[i];
21
+ }
22
+
23
+ if (total <= 0) {
24
+ throw new RangeError('Weights must sum to positive value');
18
25
  }
19
- return a + b;
20
- }, 0);
21
-
22
- if (total <= 0) {
23
- throw new Error('Weights must sum to positive value');
24
- }
25
-
26
- let rand = rng.nextFloat() * total;
27
-
28
- for (let i = 0; i < arr.length; i++) {
29
- rand -= weights[i];
30
- if (rand <= 0) return arr[i];
31
- }
32
-
33
- return arr[arr.length - 1];
34
- };
35
-
36
- export const weightedSample = (arr, weights, count, rng) => {
37
- if (!Array.isArray(arr) || arr.length === 0) {
38
- throw new TypeError('arr must be a non-empty array');
39
- }
40
- if (!Array.isArray(weights) || weights.length === 0) {
41
- throw new TypeError('weights must be a non-empty array');
42
- }
43
- if (arr.length !== weights.length) {
44
- throw new Error('arr and weights must have same length');
45
- }
46
- if (typeof count !== 'number' || !Number.isInteger(count)) {
47
- throw new TypeError('count must be an integer');
48
- }
49
- if (count <= 0) {
50
- return [];
51
- }
52
- if (!rng || typeof rng.nextFloat !== 'function') {
53
- throw new TypeError('rng must be an RNG instance');
54
- }
55
-
56
- const result = [];
57
- const len = arr.length;
58
- if (count >= len) {
59
- return [...arr];
60
- }
61
26
 
62
- const remaining = [...arr];
63
- const remainingWeights = [...weights];
27
+ let rand = rng.nextFloat() * total;
28
+ let cumsum = 0;
64
29
 
65
- for (let i = 0; i < count && remaining.length > 0; i++) {
66
- const idx = weightedPickIndex(remainingWeights, rng);
67
- result.push(remaining[idx]);
68
- remaining.splice(idx, 1);
69
- remainingWeights.splice(idx, 1);
30
+ for (let i = 0; i < arr.length; i++) {
31
+ cumsum += weights[i];
32
+ if (rand < cumsum) return arr[i];
70
33
  }
71
34
 
72
- return result;
35
+ return arr[arr.length - 1];
73
36
  };
74
37
 
75
- const weightedPickIndex = (weights, rng) => {
76
- const total = weights.reduce((a, b) => a + b, 0);
77
- let rand = rng.nextFloat() * total;
78
-
79
- for (let i = 0; i < weights.length; i++) {
80
- rand -= weights[i];
81
- if (rand <= 0) return i;
82
- }
83
-
84
- return weights.length - 1;
85
- };
38
+ export const weightedSample = (arr, weights, count, rng) => {
39
+ if (!Array.isArray(arr) || arr.length === 0) {
40
+ throw new RangeError('arr must be a non-empty array');
41
+ }
42
+ if (!Array.isArray(weights) || weights.length === 0) {
43
+ throw new RangeError('weights must be a non-empty array');
44
+ }
45
+ if (arr.length !== weights.length) {
46
+ throw new RangeError('arr and weights must have same length');
47
+ }
48
+ if (typeof count !== 'number' || !Number.isInteger(count)) {
49
+ throw new TypeError('count must be an integer');
50
+ }
51
+ if (count <= 0) {
52
+ return [];
53
+ }
54
+ if (!rng || typeof rng.nextFloat !== 'function') {
55
+ throw new TypeError('rng must be an RNG instance');
56
+ }
57
+
58
+ const len = arr.length;
59
+ if (count >= len) {
60
+ return [...arr];
61
+ }
62
+
63
+ const remaining = [...arr];
64
+ const remainingWeights = [...weights];
65
+ const result = [];
66
+
67
+ for (let i = 0; i < count; i++) {
68
+ const idx = weightedPickIndexRange(remainingWeights, i, rng);
69
+ result.push(remaining[i + idx]);
70
+
71
+ const temp = remaining[i];
72
+ remaining[i] = remaining[i + idx];
73
+ remaining[i + idx] = temp;
74
+
75
+ const tempW = remainingWeights[i];
76
+ remainingWeights[i] = remainingWeights[i + idx];
77
+ remainingWeights[i + idx] = tempW;
78
+ }
79
+
80
+ return result;
81
+ };
82
+
83
+ const weightedPickIndexRange = (weights, start, rng) => {
84
+ let total = 0;
85
+ for (let i = start; i < weights.length; i++) {
86
+ total += weights[i];
87
+ }
88
+ let rand = rng.nextFloat() * total;
89
+ for (let i = start; i < weights.length; i++) {
90
+ rand -= weights[i];
91
+ if (rand <= 0) return i - start;
92
+ }
93
+ return weights.length - start - 1;
94
+ };
86
95
 
87
96
  export const reservoirSample = (stream, k, rng) => {
88
97
  if (!Array.isArray(stream) || stream.length === 0) {
package/src/utils/seed.js CHANGED
@@ -1,10 +1,16 @@
1
1
  export const seedFromTime = () => {
2
- return BigInt(Date.now() + Math.random() * 1000000);
3
- };
2
+ const time = BigInt(Date.now());
3
+ const rand = BigInt(Math.floor(Math.random() * 1000000));
4
+ return (time << 20n) | rand;
5
+ };
4
6
 
5
7
  export const seedFromEntropy = (entropy) => {
6
- if (typeof entropy !== 'string') throw new TypeError('Entropy must be string');
7
- if (entropy.length === 0) throw new Error('Entropy cannot be empty');
8
+ if (typeof entropy !== 'string') {
9
+ throw new TypeError('entropy must be a string');
10
+ }
11
+ if (entropy.length === 0) {
12
+ throw new RangeError('entropy cannot be empty');
13
+ }
8
14
 
9
15
  let hash = 5381n;
10
16
  for (let i = 0; i < entropy.length; i++) {
@@ -22,16 +22,28 @@ export class SeedSequence {
22
22
  }
23
23
 
24
24
  spawn(n = 1) {
25
- const seeds = [];
26
- for (let i = 0; i < n; i++) {
27
- seeds.push(this.next());
28
- }
29
- return seeds;
25
+ if (typeof n !== 'number' || !Number.isInteger(n)) {
26
+ throw new TypeError('n must be an integer');
27
+ }
28
+ if (n <= 0) {
29
+ throw new RangeError('n must be positive');
30
+ }
31
+ const seeds = [];
32
+ for (let i = 0; i < n; i++) {
33
+ seeds.push(this.next());
34
+ }
35
+ return seeds;
36
+ }
30
37
  }
31
- }
32
-
33
- export const seedMultiple = (rngClasses, entropy = null) => {
34
- const seq = new SeedSequence(entropy);
35
- const seeds = seq.spawn(rngClasses.length);
36
- return rngClasses.map((RngClass, i) => new RngClass(seeds[i]));
37
- };
38
+
39
+ export const seedMultiple = (rngClasses, entropy = null) => {
40
+ if (!Array.isArray(rngClasses)) {
41
+ throw new TypeError('rngClasses must be an array');
42
+ }
43
+ if (rngClasses.length === 0) {
44
+ throw new RangeError('rngClasses cannot be empty');
45
+ }
46
+ const seq = new SeedSequence(entropy);
47
+ const seeds = seq.spawn(rngClasses.length);
48
+ return rngClasses.map((RngClass, i) => new RngClass(seeds[i]));
49
+ };
@@ -3,7 +3,7 @@ export const shuffle = (arr, rng, inPlace = false) => {
3
3
  throw new TypeError('First argument must be array');
4
4
  }
5
5
  if (!rng || typeof rng.nextInt !== 'function') {
6
- throw new TypeError('RNG instance required');
6
+ throw new TypeError('Second argument must be RNG instance');
7
7
  }
8
8
 
9
9
  const target = inPlace ? arr : [...arr];
@@ -20,43 +20,127 @@ export const shuffle = (arr, rng, inPlace = false) => {
20
20
  };
21
21
 
22
22
  export const pick = (arr, rng) => {
23
+ if (!Array.isArray(arr)) {
24
+ throw new TypeError('First argument must be array');
25
+ }
26
+ if (arr.length === 0) {
27
+ throw new RangeError('Array cannot be empty');
28
+ }
29
+ if (!rng || typeof rng.nextInt !== 'function') {
30
+ throw new TypeError('Second argument must be RNG instance');
31
+ }
32
+
33
+ return arr[rng.nextInt(arr.length)];
34
+ };
35
+
36
+ export const sample = (arr, count, rng) => {
37
+ if (!Array.isArray(arr)) {
38
+ throw new TypeError('First argument must be array');
39
+ }
40
+ if (typeof count !== 'number' || !Number.isInteger(count)) {
41
+ throw new TypeError('Sample count must be an integer');
42
+ }
43
+ if (count <= 0) {
44
+ throw new RangeError('Sample count must be positive');
45
+ }
46
+ if (count > arr.length) {
47
+ throw new RangeError('Sample count exceeds array length');
48
+ }
49
+ if (!rng || typeof rng.nextInt !== 'function') {
50
+ throw new TypeError('Second argument must be RNG instance');
51
+ }
52
+
53
+ const copy = [...arr];
54
+ const len = copy.length;
55
+ for (let i = len - 1; i > len - count - 1; i--) {
56
+ const j = rng.nextInt(i + 1);
57
+ const temp = copy[i];
58
+ copy[i] = copy[j];
59
+ copy[j] = temp;
60
+ }
61
+ return copy.slice(len - count);
62
+ };
63
+
64
+ export const sampleWithReplacement = (arr, count, rng) => {
23
65
  if (!Array.isArray(arr)) {
24
66
  throw new TypeError('First argument must be array');
25
67
  }
26
68
  if (arr.length === 0) {
27
- throw new Error('Array cannot be empty');
69
+ throw new RangeError('Array cannot be empty');
70
+ }
71
+ if (typeof count !== 'number' || !Number.isInteger(count)) {
72
+ throw new TypeError('Sample count must be an integer');
73
+ }
74
+ if (count <= 0) {
75
+ throw new RangeError('Sample count must be positive');
28
76
  }
29
77
  if (!rng || typeof rng.nextInt !== 'function') {
30
- throw new TypeError('RNG instance required');
78
+ throw new TypeError('rng must be an RNG instance');
31
79
  }
32
-
33
- return arr[rng.nextInt(arr.length)];
80
+
81
+ const result = new Array(count);
82
+ const len = arr.length;
83
+ for (let i = 0; i < count; i++) {
84
+ result[i] = arr[rng.nextInt(len)];
85
+ }
86
+ return result;
34
87
  };
35
88
 
36
- export const sample = (arr, count, rng) => {
37
- if (!Array.isArray(arr)) {
38
- throw new TypeError('First argument must be array');
39
- }
40
- if (typeof count !== 'number' || !Number.isInteger(count)) {
41
- throw new TypeError('Sample count must be an integer');
42
- }
43
- if (count <= 0) {
44
- throw new Error('Sample count must be positive');
45
- }
46
- if (count > arr.length) {
47
- throw new Error('Sample count exceeds array length');
48
- }
49
- if (!rng || typeof rng.nextInt !== 'function') {
50
- throw new TypeError('RNG instance required');
51
- }
52
-
53
- const copy = [...arr];
54
- const len = copy.length;
55
- for (let i = len - 1; i >= len - count; i--) {
56
- const j = rng.nextInt(i + 1);
57
- const temp = copy[i];
58
- copy[i] = copy[j];
59
- copy[j] = temp;
60
- }
61
- return copy.slice(len - count);
62
- };
89
+ export const permute = (arr, rng) => {
90
+ if (!Array.isArray(arr)) {
91
+ throw new TypeError('First argument must be array');
92
+ }
93
+ if (!rng || typeof rng.nextInt !== 'function') {
94
+ throw new TypeError('Second argument must be RNG instance');
95
+ }
96
+ return shuffle(arr, rng, false);
97
+ };
98
+
99
+ export const range = (start, end, step = 1) => {
100
+ if (typeof start !== 'number' || !Number.isInteger(start)) {
101
+ throw new TypeError('start must be an integer');
102
+ }
103
+ if (typeof end !== 'number' || !Number.isInteger(end)) {
104
+ throw new TypeError('end must be an integer');
105
+ }
106
+ if (typeof step !== 'number' || !Number.isInteger(step)) {
107
+ throw new TypeError('step must be an integer');
108
+ }
109
+ if (step === 0) {
110
+ throw new RangeError('step cannot be zero');
111
+ }
112
+
113
+ const result = [];
114
+ if (step > 0) {
115
+ for (let i = start; i < end; i += step) {
116
+ result.push(i);
117
+ }
118
+ } else {
119
+ for (let i = start; i > end; i += step) {
120
+ result.push(i);
121
+ }
122
+ }
123
+ return result;
124
+ };
125
+
126
+ export const cycle = (arr, count) => {
127
+ if (!Array.isArray(arr)) {
128
+ throw new TypeError('First argument must be array');
129
+ }
130
+ if (arr.length === 0) {
131
+ throw new RangeError('Array cannot be empty');
132
+ }
133
+ if (typeof count !== 'number' || !Number.isInteger(count)) {
134
+ throw new TypeError('count must be an integer');
135
+ }
136
+ if (count <= 0) {
137
+ throw new RangeError('count must be positive');
138
+ }
139
+
140
+ const result = new Array(count);
141
+ const len = arr.length;
142
+ for (let i = 0; i < count; i++) {
143
+ result[i] = arr[i % len];
144
+ }
145
+ return result;
146
+ };
@@ -1,39 +1,51 @@
1
1
  export const saveState = (rng) => {
2
- const gen = rng.gen || rng;
3
- if (gen.state === undefined) {
4
- throw new Error('Generator does not support state snapshots');
5
- }
6
- return { state: gen.state };
7
- };
8
-
9
- export const restoreState = (rng, snapshot) => {
10
- const gen = rng.gen || rng;
11
- if (gen.state === undefined) {
12
- throw new Error('Generator does not support state snapshots');
13
- }
14
- gen.state = snapshot.state;
15
- };
2
+ const gen = rng.gen || rng;
3
+ if (gen.state === undefined) {
4
+ throw new Error('Generator does not support state snapshots');
5
+ }
6
+ const state = gen.state;
7
+ return {
8
+ state: typeof state === 'bigint' ? state.toString() : state,
9
+ isBigInt: typeof state === 'bigint'
10
+ };
11
+ };
12
+
13
+ export const restoreState = (rng, snapshot) => {
14
+ const gen = rng.gen || rng;
15
+ if (gen.state === undefined) {
16
+ throw new Error('Generator does not support state snapshots');
17
+ }
18
+ gen.state = snapshot.isBigInt ? BigInt(snapshot.state) : snapshot.state;
19
+ };
16
20
 
17
21
  export const cloneGenerator = (rng) => {
18
- const isRNGWrapper = rng.gen !== undefined;
19
- const gen = isRNGWrapper ? rng.gen : rng;
20
- const Constructor = gen.constructor;
21
-
22
- let clonedGen;
23
-
24
- if (Constructor.name === 'PCG64') {
25
- clonedGen = new Constructor(gen.state, gen.inc);
26
- } else if (Constructor.name === 'Logistic') {
27
- clonedGen = new Constructor(gen.x, gen.r);
28
- } else if (Constructor.name === 'Tent') {
29
- clonedGen = new Constructor(gen.x, gen.mu);
30
- } else if (Constructor.name === 'Mixer') {
31
- const rng1Clone = cloneGenerator(gen.rng1);
32
- const rng2Clone = cloneGenerator(gen.rng2);
33
- clonedGen = new Constructor(rng1Clone, rng2Clone);
34
- } else {
35
- clonedGen = new Constructor(gen.state);
36
- }
37
-
38
- return isRNGWrapper ? new (rng.constructor)(Constructor, gen.state) : clonedGen;
39
- };
22
+ const isRNGWrapper = rng.gen !== undefined;
23
+ const gen = isRNGWrapper ? rng.gen : rng;
24
+ const Constructor = gen.constructor;
25
+
26
+ let clonedGen = Object.create(Constructor.prototype);
27
+
28
+ if (Constructor.name === 'PCG64') {
29
+ clonedGen.state = gen.state;
30
+ clonedGen.inc = gen.inc;
31
+ } else if (Constructor.name === 'Logistic') {
32
+ clonedGen.x = gen.x;
33
+ clonedGen.r = gen.r;
34
+ } else if (Constructor.name === 'Tent') {
35
+ clonedGen.x = gen.x;
36
+ clonedGen.mu = gen.mu;
37
+ } else if (Constructor.name === 'Xorshift64' || Constructor.name === 'Splitmix64' || Constructor.name === 'MT19937') {
38
+ clonedGen.state = gen.state;
39
+ if (Constructor.name === 'MT19937') {
40
+ clonedGen.mt = [...gen.mt];
41
+ clonedGen.mti = gen.mti;
42
+ }
43
+ } else if (Constructor.name === 'Mixer') {
44
+ clonedGen.rng1 = cloneGenerator(gen.rng1);
45
+ clonedGen.rng2 = cloneGenerator(gen.rng2);
46
+ } else {
47
+ clonedGen.state = gen.state;
48
+ }
49
+
50
+ return isRNGWrapper ? new (rng.constructor)(clonedGen) : clonedGen;
51
+ };
@@ -139,5 +139,67 @@ export const varianceTest = (data, expectedVariance = 1 / 12) => {
139
139
  expectedVariance,
140
140
  chi2Statistic: chi2,
141
141
  degreesOfFreedom: data.length - 1
142
- };
143
- };
142
+ };
143
+ };
144
+
145
+ export const skewness = (data) => {
146
+ if (!Array.isArray(data) || data.length < 3) {
147
+ throw new Error('Data must be array with at least 3 elements');
148
+ }
149
+
150
+ const n = data.length;
151
+ const mean = data.reduce((a, b) => a + b, 0) / n;
152
+ const m2 = data.reduce((sum, x) => sum + (x - mean) ** 2, 0) / n;
153
+ const m3 = data.reduce((sum, x) => sum + (x - mean) ** 3, 0) / n;
154
+ const std = Math.sqrt(m2);
155
+
156
+ if (std === 0) return 0;
157
+ return m3 / (std ** 3);
158
+ };
159
+
160
+ export const kurtosis = (data) => {
161
+ if (!Array.isArray(data) || data.length < 4) {
162
+ throw new Error('Data must be array with at least 4 elements');
163
+ }
164
+
165
+ const n = data.length;
166
+ const mean = data.reduce((a, b) => a + b, 0) / n;
167
+ const m2 = data.reduce((sum, x) => sum + (x - mean) ** 2, 0) / n;
168
+ const m4 = data.reduce((sum, x) => sum + (x - mean) ** 4, 0) / n;
169
+ const std = Math.sqrt(m2);
170
+
171
+ if (std === 0) return 0;
172
+ return m4 / (std ** 4) - 3;
173
+ };
174
+
175
+ export const median = (data) => {
176
+ if (!Array.isArray(data) || data.length === 0) {
177
+ throw new Error('Data must be non-empty array');
178
+ }
179
+
180
+ const sorted = [...data].sort((a, b) => a - b);
181
+ const mid = Math.floor(sorted.length / 2);
182
+
183
+ if (sorted.length % 2 === 1) {
184
+ return sorted[mid];
185
+ }
186
+ return (sorted[mid - 1] + sorted[mid]) / 2;
187
+ };
188
+
189
+ export const quantile = (data, q) => {
190
+ if (!Array.isArray(data) || data.length === 0) {
191
+ throw new Error('Data must be non-empty array');
192
+ }
193
+ if (typeof q !== 'number' || q < 0 || q > 1) {
194
+ throw new RangeError('Quantile must be between 0 and 1');
195
+ }
196
+
197
+ const sorted = [...data].sort((a, b) => a - b);
198
+ const idx = q * (sorted.length - 1);
199
+ const lower = Math.floor(idx);
200
+ const upper = Math.ceil(idx);
201
+ const weight = idx % 1;
202
+
203
+ if (lower === upper) return sorted[lower];
204
+ return sorted[lower] * (1 - weight) + sorted[upper] * weight;
205
+ };