@flighthq/math 0.1.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.
- package/dist/angle.d.ts +13 -0
- package/dist/angle.d.ts.map +1 -0
- package/dist/angle.js +24 -0
- package/dist/angle.js.map +1 -0
- package/dist/clamp.d.ts +15 -0
- package/dist/clamp.d.ts.map +1 -0
- package/dist/clamp.js +23 -0
- package/dist/clamp.js.map +1 -0
- package/dist/comparison.d.ts +17 -0
- package/dist/comparison.d.ts.map +1 -0
- package/dist/comparison.js +26 -0
- package/dist/comparison.js.map +1 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +11 -0
- package/dist/constants.js.map +1 -0
- package/dist/hash.d.ts +35 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +51 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/interpolation.d.ts +27 -0
- package/dist/interpolation.d.ts.map +1 -0
- package/dist/interpolation.js +43 -0
- package/dist/interpolation.js.map +1 -0
- package/dist/interpolationAdvanced.d.ts +42 -0
- package/dist/interpolationAdvanced.d.ts.map +1 -0
- package/dist/interpolationAdvanced.js +71 -0
- package/dist/interpolationAdvanced.js.map +1 -0
- package/dist/nextPowerOfTwo.d.ts +22 -0
- package/dist/nextPowerOfTwo.d.ts.map +1 -0
- package/dist/nextPowerOfTwo.js +52 -0
- package/dist/nextPowerOfTwo.js.map +1 -0
- package/dist/numberTheory.d.ts +33 -0
- package/dist/numberTheory.d.ts.map +1 -0
- package/dist/numberTheory.js +62 -0
- package/dist/numberTheory.js.map +1 -0
- package/dist/random.d.ts +20 -0
- package/dist/random.d.ts.map +1 -0
- package/dist/random.js +27 -0
- package/dist/random.js.map +1 -0
- package/dist/randomDistributions.d.ts +105 -0
- package/dist/randomDistributions.d.ts.map +1 -0
- package/dist/randomDistributions.js +209 -0
- package/dist/randomDistributions.js.map +1 -0
- package/dist/randomRange.d.ts +18 -0
- package/dist/randomRange.d.ts.map +1 -0
- package/dist/randomRange.js +29 -0
- package/dist/randomRange.js.map +1 -0
- package/dist/rounding.d.ts +35 -0
- package/dist/rounding.d.ts.map +1 -0
- package/dist/rounding.js +53 -0
- package/dist/rounding.js.map +1 -0
- package/dist/scalar.d.ts +30 -0
- package/dist/scalar.d.ts.map +1 -0
- package/dist/scalar.js +42 -0
- package/dist/scalar.js.map +1 -0
- package/dist/statistics.d.ts +29 -0
- package/dist/statistics.d.ts.map +1 -0
- package/dist/statistics.js +67 -0
- package/dist/statistics.js.map +1 -0
- package/package.json +37 -0
- package/src/angle.test.ts +83 -0
- package/src/clamp.test.ts +61 -0
- package/src/comparison.test.ts +54 -0
- package/src/constants.test.ts +38 -0
- package/src/hash.test.ts +84 -0
- package/src/interpolation.test.ts +93 -0
- package/src/interpolationAdvanced.test.ts +122 -0
- package/src/nextPowerOfTwo.test.ts +84 -0
- package/src/numberTheory.test.ts +95 -0
- package/src/random.test.ts +32 -0
- package/src/randomDistributions.test.ts +348 -0
- package/src/randomRange.test.ts +99 -0
- package/src/rounding.test.ts +81 -0
- package/src/scalar.test.ts +58 -0
- package/src/statistics.test.ts +74 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { damp, lerpAngle, moveTowards, pingPong, repeat, smootherStep } from './interpolationAdvanced';
|
|
2
|
+
|
|
3
|
+
describe('damp', () => {
|
|
4
|
+
it('moves current toward target over time', () => {
|
|
5
|
+
const result = damp(0, 10, 1, 1);
|
|
6
|
+
expect(result).toBeGreaterThan(0);
|
|
7
|
+
expect(result).toBeLessThan(10);
|
|
8
|
+
});
|
|
9
|
+
it('returns current when deltaTime is 0', () => {
|
|
10
|
+
expect(damp(5, 10, 2, 0)).toBe(5);
|
|
11
|
+
});
|
|
12
|
+
it('returns current when lambda is 0', () => {
|
|
13
|
+
expect(damp(5, 10, 0, 1)).toBe(5);
|
|
14
|
+
});
|
|
15
|
+
it('approaches target asymptotically', () => {
|
|
16
|
+
const stepValue = damp(0, 1, 5, 0.1);
|
|
17
|
+
expect(stepValue).toBeGreaterThan(0.39);
|
|
18
|
+
expect(stepValue).toBeLessThan(0.41);
|
|
19
|
+
});
|
|
20
|
+
it('is frame-rate independent: two half-steps equal one full step', () => {
|
|
21
|
+
const full = damp(0, 1, 2, 0.2);
|
|
22
|
+
const half1 = damp(0, 1, 2, 0.1);
|
|
23
|
+
const half2 = damp(half1, 1, 2, 0.1);
|
|
24
|
+
expect(half2).toBeCloseTo(full, 10);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('lerpAngle', () => {
|
|
29
|
+
it('interpolates between two close angles', () => {
|
|
30
|
+
const result = lerpAngle(0, Math.PI / 2, 0.5);
|
|
31
|
+
expect(result).toBeCloseTo(Math.PI / 4, 10);
|
|
32
|
+
});
|
|
33
|
+
it('takes the shortest arc across 0/2π', () => {
|
|
34
|
+
// 1.9π → 0.1π: the short arc is +0.2π. At t = 0.5 the result is 1.9π + 0.1π = 2π.
|
|
35
|
+
// 2π and 0 are the same angle; normalise before comparing.
|
|
36
|
+
const TAU = Math.PI * 2;
|
|
37
|
+
const result = lerpAngle(Math.PI * 1.9, Math.PI * 0.1, 0.5);
|
|
38
|
+
const normalised = ((result % TAU) + TAU) % TAU;
|
|
39
|
+
expect(normalised).toBeCloseTo(0, 5);
|
|
40
|
+
});
|
|
41
|
+
it('returns a at t = 0', () => {
|
|
42
|
+
expect(lerpAngle(1, 2, 0)).toBeCloseTo(1, 10);
|
|
43
|
+
});
|
|
44
|
+
it('returns b at t = 1', () => {
|
|
45
|
+
expect(lerpAngle(1, 2, 1)).toBeCloseTo(2, 10);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('moveTowards', () => {
|
|
50
|
+
it('moves toward target by maxDelta', () => {
|
|
51
|
+
expect(moveTowards(0, 10, 3)).toBe(3);
|
|
52
|
+
});
|
|
53
|
+
it('snaps to target when within maxDelta', () => {
|
|
54
|
+
expect(moveTowards(9, 10, 5)).toBe(10);
|
|
55
|
+
});
|
|
56
|
+
it('works when target is below current', () => {
|
|
57
|
+
expect(moveTowards(10, 0, 3)).toBe(7);
|
|
58
|
+
});
|
|
59
|
+
it('returns target when current equals target', () => {
|
|
60
|
+
expect(moveTowards(5, 5, 1)).toBe(5);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('pingPong', () => {
|
|
65
|
+
it('starts at 0', () => {
|
|
66
|
+
expect(pingPong(0, 1)).toBe(0);
|
|
67
|
+
});
|
|
68
|
+
it('reaches length at t = length', () => {
|
|
69
|
+
expect(pingPong(1, 1)).toBe(1);
|
|
70
|
+
});
|
|
71
|
+
it('bounces back at t = 1.5 * length', () => {
|
|
72
|
+
expect(pingPong(1.5, 1)).toBeCloseTo(0.5, 10);
|
|
73
|
+
});
|
|
74
|
+
it('returns 0 when length is 0', () => {
|
|
75
|
+
expect(pingPong(1, 0)).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
it('works with larger lengths', () => {
|
|
78
|
+
expect(pingPong(3, 2)).toBeCloseTo(1, 10);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('repeat', () => {
|
|
83
|
+
it('wraps t over [0, length)', () => {
|
|
84
|
+
expect(repeat(1.6, 1)).toBeCloseTo(0.6, 10);
|
|
85
|
+
});
|
|
86
|
+
it('returns 0 for exact multiple', () => {
|
|
87
|
+
expect(repeat(2, 1)).toBeCloseTo(0, 10);
|
|
88
|
+
});
|
|
89
|
+
it('handles negative t', () => {
|
|
90
|
+
const r = repeat(-0.3, 1);
|
|
91
|
+
expect(r).toBeGreaterThanOrEqual(0);
|
|
92
|
+
expect(r).toBeLessThan(1);
|
|
93
|
+
});
|
|
94
|
+
it('returns 0 when length is 0', () => {
|
|
95
|
+
expect(repeat(1, 0)).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('smootherStep', () => {
|
|
100
|
+
it('returns 0 at the lower edge', () => {
|
|
101
|
+
expect(smootherStep(0, 1, 0)).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
it('returns 1 at the upper edge', () => {
|
|
104
|
+
expect(smootherStep(0, 1, 1)).toBe(1);
|
|
105
|
+
});
|
|
106
|
+
it('returns 0.5 at the midpoint', () => {
|
|
107
|
+
expect(smootherStep(0, 1, 0.5)).toBe(0.5);
|
|
108
|
+
});
|
|
109
|
+
it('returns 0 for x below the lower edge', () => {
|
|
110
|
+
expect(smootherStep(0, 1, -1)).toBe(0);
|
|
111
|
+
});
|
|
112
|
+
it('returns 1 for x above the upper edge', () => {
|
|
113
|
+
expect(smootherStep(0, 1, 2)).toBe(1);
|
|
114
|
+
});
|
|
115
|
+
it('has zero first and second derivatives at the edges', () => {
|
|
116
|
+
const h = 0.001;
|
|
117
|
+
const dLow = (smootherStep(0, 1, h) - smootherStep(0, 1, 0)) / h;
|
|
118
|
+
const dHigh = (smootherStep(0, 1, 1) - smootherStep(0, 1, 1 - h)) / h;
|
|
119
|
+
expect(dLow).toBeCloseTo(0, 1);
|
|
120
|
+
expect(dHigh).toBeCloseTo(0, 1);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { isPowerOfTwo, nextMultipleOf, nextPowerOfTwo, previousPowerOfTwo } from './nextPowerOfTwo';
|
|
2
|
+
|
|
3
|
+
describe('isPowerOfTwo', () => {
|
|
4
|
+
it('returns true for powers of two', () => {
|
|
5
|
+
expect(isPowerOfTwo(1)).toBe(true);
|
|
6
|
+
expect(isPowerOfTwo(2)).toBe(true);
|
|
7
|
+
expect(isPowerOfTwo(4)).toBe(true);
|
|
8
|
+
expect(isPowerOfTwo(64)).toBe(true);
|
|
9
|
+
expect(isPowerOfTwo(1024)).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
it('returns false for non-powers of two', () => {
|
|
12
|
+
expect(isPowerOfTwo(3)).toBe(false);
|
|
13
|
+
expect(isPowerOfTwo(5)).toBe(false);
|
|
14
|
+
expect(isPowerOfTwo(100)).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
it('returns false for 0', () => {
|
|
17
|
+
expect(isPowerOfTwo(0)).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
it('returns false for negative numbers', () => {
|
|
20
|
+
expect(isPowerOfTwo(-1)).toBe(false);
|
|
21
|
+
expect(isPowerOfTwo(-4)).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('nextMultipleOf', () => {
|
|
26
|
+
it('rounds up to the next multiple', () => {
|
|
27
|
+
expect(nextMultipleOf(7, 4)).toBe(8);
|
|
28
|
+
});
|
|
29
|
+
it('leaves an exact multiple unchanged', () => {
|
|
30
|
+
expect(nextMultipleOf(8, 4)).toBe(8);
|
|
31
|
+
});
|
|
32
|
+
it('returns value when multiple is 0', () => {
|
|
33
|
+
expect(nextMultipleOf(7, 0)).toBe(7);
|
|
34
|
+
});
|
|
35
|
+
it('works with small values', () => {
|
|
36
|
+
expect(nextMultipleOf(1, 16)).toBe(16);
|
|
37
|
+
expect(nextMultipleOf(0, 4)).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('nextPowerOfTwo', () => {
|
|
42
|
+
it('returns 1 for 0', () => {
|
|
43
|
+
expect(nextPowerOfTwo(0)).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
it('returns 1 for 1', () => {
|
|
46
|
+
expect(nextPowerOfTwo(1)).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
it('returns an exact power of two unchanged', () => {
|
|
49
|
+
expect(nextPowerOfTwo(2)).toBe(2);
|
|
50
|
+
expect(nextPowerOfTwo(64)).toBe(64);
|
|
51
|
+
expect(nextPowerOfTwo(128)).toBe(128);
|
|
52
|
+
expect(nextPowerOfTwo(1024)).toBe(1024);
|
|
53
|
+
});
|
|
54
|
+
it('rounds up to the next power of two', () => {
|
|
55
|
+
expect(nextPowerOfTwo(3)).toBe(4);
|
|
56
|
+
expect(nextPowerOfTwo(5)).toBe(8);
|
|
57
|
+
expect(nextPowerOfTwo(100)).toBe(128);
|
|
58
|
+
expect(nextPowerOfTwo(129)).toBe(256);
|
|
59
|
+
expect(nextPowerOfTwo(1000)).toBe(1024);
|
|
60
|
+
});
|
|
61
|
+
it('handles negative numbers', () => {
|
|
62
|
+
expect(nextPowerOfTwo(-1)).toBe(1);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('previousPowerOfTwo', () => {
|
|
67
|
+
it('returns 1 for 1', () => {
|
|
68
|
+
expect(previousPowerOfTwo(1)).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
it('returns 1 for values <= 1', () => {
|
|
71
|
+
expect(previousPowerOfTwo(0)).toBe(1);
|
|
72
|
+
expect(previousPowerOfTwo(-1)).toBe(1);
|
|
73
|
+
});
|
|
74
|
+
it('returns an exact power of two unchanged', () => {
|
|
75
|
+
expect(previousPowerOfTwo(4)).toBe(4);
|
|
76
|
+
expect(previousPowerOfTwo(64)).toBe(64);
|
|
77
|
+
});
|
|
78
|
+
it('rounds down to the previous power of two', () => {
|
|
79
|
+
expect(previousPowerOfTwo(6)).toBe(4);
|
|
80
|
+
expect(previousPowerOfTwo(100)).toBe(64);
|
|
81
|
+
expect(previousPowerOfTwo(200)).toBe(128);
|
|
82
|
+
expect(previousPowerOfTwo(1025)).toBe(1024);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { factorial, gcd, hypot2, isEven, isOdd, lcm } from './numberTheory';
|
|
2
|
+
|
|
3
|
+
describe('factorial', () => {
|
|
4
|
+
it('returns 1 for 0', () => {
|
|
5
|
+
expect(factorial(0)).toBe(1);
|
|
6
|
+
});
|
|
7
|
+
it('returns 1 for 1', () => {
|
|
8
|
+
expect(factorial(1)).toBe(1);
|
|
9
|
+
});
|
|
10
|
+
it('computes common factorials', () => {
|
|
11
|
+
expect(factorial(5)).toBe(120);
|
|
12
|
+
expect(factorial(10)).toBe(3628800);
|
|
13
|
+
});
|
|
14
|
+
it('throws for negative integers', () => {
|
|
15
|
+
expect(() => factorial(-1)).toThrow(RangeError);
|
|
16
|
+
});
|
|
17
|
+
it('throws for non-integers', () => {
|
|
18
|
+
expect(() => factorial(1.5)).toThrow(RangeError);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('gcd', () => {
|
|
23
|
+
it('returns the gcd of two positive integers', () => {
|
|
24
|
+
expect(gcd(12, 8)).toBe(4);
|
|
25
|
+
expect(gcd(100, 75)).toBe(25);
|
|
26
|
+
});
|
|
27
|
+
it('returns the larger number when one is a multiple of the other', () => {
|
|
28
|
+
expect(gcd(6, 3)).toBe(3);
|
|
29
|
+
});
|
|
30
|
+
it('returns 1 for coprime numbers', () => {
|
|
31
|
+
expect(gcd(7, 13)).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
it('accepts negative inputs', () => {
|
|
34
|
+
expect(gcd(-12, 8)).toBe(4);
|
|
35
|
+
});
|
|
36
|
+
it('accepts zero as one argument', () => {
|
|
37
|
+
expect(gcd(0, 5)).toBe(5);
|
|
38
|
+
expect(gcd(7, 0)).toBe(7);
|
|
39
|
+
});
|
|
40
|
+
it('throws when both arguments are 0', () => {
|
|
41
|
+
expect(() => gcd(0, 0)).toThrow(RangeError);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('hypot2', () => {
|
|
46
|
+
it('returns x squared plus y squared', () => {
|
|
47
|
+
expect(hypot2(3, 4)).toBe(25);
|
|
48
|
+
});
|
|
49
|
+
it('returns 0 for zero inputs', () => {
|
|
50
|
+
expect(hypot2(0, 0)).toBe(0);
|
|
51
|
+
});
|
|
52
|
+
it('matches Math.hypot squared', () => {
|
|
53
|
+
const h = Math.hypot(5, 12);
|
|
54
|
+
expect(hypot2(5, 12)).toBeCloseTo(h * h, 10);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('isEven', () => {
|
|
59
|
+
it('returns true for even numbers', () => {
|
|
60
|
+
expect(isEven(0)).toBe(true);
|
|
61
|
+
expect(isEven(2)).toBe(true);
|
|
62
|
+
expect(isEven(100)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it('returns false for odd numbers', () => {
|
|
65
|
+
expect(isEven(1)).toBe(false);
|
|
66
|
+
expect(isEven(3)).toBe(false);
|
|
67
|
+
expect(isEven(99)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('isOdd', () => {
|
|
72
|
+
it('returns true for odd numbers', () => {
|
|
73
|
+
expect(isOdd(1)).toBe(true);
|
|
74
|
+
expect(isOdd(3)).toBe(true);
|
|
75
|
+
expect(isOdd(99)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
it('returns false for even numbers', () => {
|
|
78
|
+
expect(isOdd(0)).toBe(false);
|
|
79
|
+
expect(isOdd(2)).toBe(false);
|
|
80
|
+
expect(isOdd(100)).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('lcm', () => {
|
|
85
|
+
it('returns the lcm of two positive integers', () => {
|
|
86
|
+
expect(lcm(4, 6)).toBe(12);
|
|
87
|
+
expect(lcm(3, 5)).toBe(15);
|
|
88
|
+
});
|
|
89
|
+
it('returns the larger number when one is a multiple of the other', () => {
|
|
90
|
+
expect(lcm(4, 8)).toBe(8);
|
|
91
|
+
});
|
|
92
|
+
it('accepts negative inputs', () => {
|
|
93
|
+
expect(lcm(-4, 6)).toBe(12);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createRandomSource } from './random';
|
|
2
|
+
|
|
3
|
+
describe('createRandomSource', () => {
|
|
4
|
+
it('produces values in [0, 1)', () => {
|
|
5
|
+
const rng = createRandomSource(1);
|
|
6
|
+
for (let i = 0; i < 1000; i++) {
|
|
7
|
+
const v = rng();
|
|
8
|
+
expect(v).toBeGreaterThanOrEqual(0);
|
|
9
|
+
expect(v).toBeLessThan(1);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('is deterministic — same seed yields the same sequence', () => {
|
|
14
|
+
const a = createRandomSource(0xc0ffee);
|
|
15
|
+
const b = createRandomSource(0xc0ffee);
|
|
16
|
+
for (let i = 0; i < 100; i++) expect(a()).toBe(b());
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('different seeds yield different sequences', () => {
|
|
20
|
+
const a = createRandomSource(1);
|
|
21
|
+
const b = createRandomSource(2);
|
|
22
|
+
let differs = false;
|
|
23
|
+
for (let i = 0; i < 10; i++) if (a() !== b()) differs = true;
|
|
24
|
+
expect(differs).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('tolerates non-finite seeds without producing NaN', () => {
|
|
28
|
+
const rng = createRandomSource(NaN);
|
|
29
|
+
const v = rng();
|
|
30
|
+
expect(Number.isFinite(v)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { createRandomSource } from './random';
|
|
2
|
+
import {
|
|
3
|
+
pick,
|
|
4
|
+
randomExponential,
|
|
5
|
+
randomGaussian,
|
|
6
|
+
randomGaussianPair,
|
|
7
|
+
randomInsideUnitDisc,
|
|
8
|
+
randomInsideUnitSphere,
|
|
9
|
+
randomOnUnitCircle,
|
|
10
|
+
randomOnUnitSphere,
|
|
11
|
+
randomPoisson,
|
|
12
|
+
randomWeighted,
|
|
13
|
+
shuffle,
|
|
14
|
+
shuffleInPlace,
|
|
15
|
+
} from './randomDistributions';
|
|
16
|
+
|
|
17
|
+
const rng = () => createRandomSource(0xabcdef);
|
|
18
|
+
|
|
19
|
+
describe('pick', () => {
|
|
20
|
+
it('returns an element from the array', () => {
|
|
21
|
+
const items = [1, 2, 3, 4, 5];
|
|
22
|
+
const random = rng();
|
|
23
|
+
for (let i = 0; i < 50; i++) {
|
|
24
|
+
expect(items).toContain(pick(random, items));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
it('returns undefined for an empty array', () => {
|
|
28
|
+
expect(pick(rng(), [])).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
it('always returns the only element in a single-item array', () => {
|
|
31
|
+
const random = rng();
|
|
32
|
+
for (let i = 0; i < 20; i++) expect(pick(random, [42])).toBe(42);
|
|
33
|
+
});
|
|
34
|
+
it('is deterministic for the same seed', () => {
|
|
35
|
+
const items = ['a', 'b', 'c', 'd'];
|
|
36
|
+
const a = rng();
|
|
37
|
+
const b = rng();
|
|
38
|
+
for (let i = 0; i < 20; i++) expect(pick(a, items)).toBe(pick(b, items));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('randomExponential', () => {
|
|
43
|
+
it('returns a non-negative value', () => {
|
|
44
|
+
const random = rng();
|
|
45
|
+
for (let i = 0; i < 200; i++) {
|
|
46
|
+
expect(randomExponential(random)).toBeGreaterThanOrEqual(0);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
it('mean approximates 1/rate for rate=1', () => {
|
|
50
|
+
const random = rng();
|
|
51
|
+
let sum = 0;
|
|
52
|
+
const n = 5000;
|
|
53
|
+
for (let i = 0; i < n; i++) sum += randomExponential(random, 1);
|
|
54
|
+
expect(sum / n).toBeCloseTo(1, 0);
|
|
55
|
+
});
|
|
56
|
+
it('mean approximates 1/rate for rate=2', () => {
|
|
57
|
+
const random = rng();
|
|
58
|
+
let sum = 0;
|
|
59
|
+
const n = 5000;
|
|
60
|
+
for (let i = 0; i < n; i++) sum += randomExponential(random, 2);
|
|
61
|
+
expect(sum / n).toBeCloseTo(0.5, 0);
|
|
62
|
+
});
|
|
63
|
+
it('throws for rate <= 0', () => {
|
|
64
|
+
expect(() => randomExponential(rng(), 0)).toThrow(RangeError);
|
|
65
|
+
expect(() => randomExponential(rng(), -1)).toThrow(RangeError);
|
|
66
|
+
});
|
|
67
|
+
it('is deterministic for the same seed', () => {
|
|
68
|
+
const a = rng();
|
|
69
|
+
const b = rng();
|
|
70
|
+
for (let i = 0; i < 20; i++) expect(randomExponential(a)).toBe(randomExponential(b));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('randomGaussian', () => {
|
|
75
|
+
it('produces finite values', () => {
|
|
76
|
+
const random = rng();
|
|
77
|
+
for (let i = 0; i < 100; i++) expect(Number.isFinite(randomGaussian(random))).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
it('centers around the mean', () => {
|
|
80
|
+
const random = rng();
|
|
81
|
+
let sum = 0;
|
|
82
|
+
const n = 2000;
|
|
83
|
+
for (let i = 0; i < n; i++) sum += randomGaussian(random, 5, 1);
|
|
84
|
+
expect(sum / n).toBeCloseTo(5, 0);
|
|
85
|
+
});
|
|
86
|
+
it('is deterministic for the same seed', () => {
|
|
87
|
+
const a = rng();
|
|
88
|
+
const b = rng();
|
|
89
|
+
for (let i = 0; i < 20; i++) expect(randomGaussian(a)).toBe(randomGaussian(b));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('randomGaussianPair', () => {
|
|
94
|
+
it('returns a tuple of two finite values', () => {
|
|
95
|
+
const random = rng();
|
|
96
|
+
const [z0, z1] = randomGaussianPair(random);
|
|
97
|
+
expect(Number.isFinite(z0)).toBe(true);
|
|
98
|
+
expect(Number.isFinite(z1)).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
it('produces values matching the requested mean', () => {
|
|
101
|
+
const random = rng();
|
|
102
|
+
let sum = 0;
|
|
103
|
+
const n = 2000;
|
|
104
|
+
for (let i = 0; i < n; i++) {
|
|
105
|
+
const [z0, z1] = randomGaussianPair(random, 10, 1);
|
|
106
|
+
sum += z0 + z1;
|
|
107
|
+
}
|
|
108
|
+
expect(sum / (n * 2)).toBeCloseTo(10, 0);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('randomInsideUnitDisc', () => {
|
|
113
|
+
it('produces points inside the unit disc', () => {
|
|
114
|
+
const random = rng();
|
|
115
|
+
const out = { x: 0, y: 0 };
|
|
116
|
+
for (let i = 0; i < 200; i++) {
|
|
117
|
+
randomInsideUnitDisc(random, out);
|
|
118
|
+
expect(out.x * out.x + out.y * out.y).toBeLessThanOrEqual(1 + 1e-9);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
it('is alias-safe when out is the same object as input', () => {
|
|
122
|
+
const random = rng();
|
|
123
|
+
const out = { x: 0, y: 0 };
|
|
124
|
+
// Should not throw or produce garbage when aliased
|
|
125
|
+
randomInsideUnitDisc(random, out);
|
|
126
|
+
expect(Number.isFinite(out.x)).toBe(true);
|
|
127
|
+
expect(Number.isFinite(out.y)).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
it('is deterministic for the same seed', () => {
|
|
130
|
+
const a = rng();
|
|
131
|
+
const b = rng();
|
|
132
|
+
const outA = { x: 0, y: 0 };
|
|
133
|
+
const outB = { x: 0, y: 0 };
|
|
134
|
+
for (let i = 0; i < 20; i++) {
|
|
135
|
+
randomInsideUnitDisc(a, outA);
|
|
136
|
+
randomInsideUnitDisc(b, outB);
|
|
137
|
+
expect(outA.x).toBe(outB.x);
|
|
138
|
+
expect(outA.y).toBe(outB.y);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('randomInsideUnitSphere', () => {
|
|
144
|
+
it('produces points with radius <= 1', () => {
|
|
145
|
+
const random = rng();
|
|
146
|
+
const out = { x: 0, y: 0, z: 0 };
|
|
147
|
+
for (let i = 0; i < 200; i++) {
|
|
148
|
+
randomInsideUnitSphere(random, out);
|
|
149
|
+
expect(out.x * out.x + out.y * out.y + out.z * out.z).toBeLessThanOrEqual(1 + 1e-9);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
it('is alias-safe when out is the same object as input', () => {
|
|
153
|
+
const random = rng();
|
|
154
|
+
const out = { x: 0, y: 0, z: 0 };
|
|
155
|
+
randomInsideUnitSphere(random, out);
|
|
156
|
+
expect(Number.isFinite(out.x)).toBe(true);
|
|
157
|
+
expect(Number.isFinite(out.y)).toBe(true);
|
|
158
|
+
expect(Number.isFinite(out.z)).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
it('is deterministic for the same seed', () => {
|
|
161
|
+
const a = rng();
|
|
162
|
+
const b = rng();
|
|
163
|
+
const outA = { x: 0, y: 0, z: 0 };
|
|
164
|
+
const outB = { x: 0, y: 0, z: 0 };
|
|
165
|
+
for (let i = 0; i < 20; i++) {
|
|
166
|
+
randomInsideUnitSphere(a, outA);
|
|
167
|
+
randomInsideUnitSphere(b, outB);
|
|
168
|
+
expect(outA.x).toBe(outB.x);
|
|
169
|
+
expect(outA.y).toBe(outB.y);
|
|
170
|
+
expect(outA.z).toBe(outB.z);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('randomOnUnitCircle', () => {
|
|
176
|
+
it('produces points on the unit circle', () => {
|
|
177
|
+
const random = rng();
|
|
178
|
+
const out = { x: 0, y: 0 };
|
|
179
|
+
for (let i = 0; i < 100; i++) {
|
|
180
|
+
randomOnUnitCircle(random, out);
|
|
181
|
+
expect(out.x * out.x + out.y * out.y).toBeCloseTo(1, 8);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
it('is alias-safe when out is the same object as input', () => {
|
|
185
|
+
const random = rng();
|
|
186
|
+
const out = { x: 0, y: 0 };
|
|
187
|
+
randomOnUnitCircle(random, out);
|
|
188
|
+
expect(Number.isFinite(out.x)).toBe(true);
|
|
189
|
+
expect(Number.isFinite(out.y)).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
it('is deterministic for the same seed', () => {
|
|
192
|
+
const a = rng();
|
|
193
|
+
const b = rng();
|
|
194
|
+
const outA = { x: 0, y: 0 };
|
|
195
|
+
const outB = { x: 0, y: 0 };
|
|
196
|
+
for (let i = 0; i < 20; i++) {
|
|
197
|
+
randomOnUnitCircle(a, outA);
|
|
198
|
+
randomOnUnitCircle(b, outB);
|
|
199
|
+
expect(outA.x).toBe(outB.x);
|
|
200
|
+
expect(outA.y).toBe(outB.y);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('randomOnUnitSphere', () => {
|
|
206
|
+
it('produces points on the unit sphere', () => {
|
|
207
|
+
const random = rng();
|
|
208
|
+
const out = { x: 0, y: 0, z: 0 };
|
|
209
|
+
for (let i = 0; i < 100; i++) {
|
|
210
|
+
randomOnUnitSphere(random, out);
|
|
211
|
+
const len2 = out.x * out.x + out.y * out.y + out.z * out.z;
|
|
212
|
+
expect(len2).toBeCloseTo(1, 8);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
it('is alias-safe when out is the same object as input', () => {
|
|
216
|
+
const random = rng();
|
|
217
|
+
const out = { x: 0, y: 0, z: 0 };
|
|
218
|
+
randomOnUnitSphere(random, out);
|
|
219
|
+
expect(Number.isFinite(out.x)).toBe(true);
|
|
220
|
+
expect(Number.isFinite(out.y)).toBe(true);
|
|
221
|
+
expect(Number.isFinite(out.z)).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
it('is deterministic for the same seed', () => {
|
|
224
|
+
const a = rng();
|
|
225
|
+
const b = rng();
|
|
226
|
+
const outA = { x: 0, y: 0, z: 0 };
|
|
227
|
+
const outB = { x: 0, y: 0, z: 0 };
|
|
228
|
+
for (let i = 0; i < 20; i++) {
|
|
229
|
+
randomOnUnitSphere(a, outA);
|
|
230
|
+
randomOnUnitSphere(b, outB);
|
|
231
|
+
expect(outA.x).toBe(outB.x);
|
|
232
|
+
expect(outA.y).toBe(outB.y);
|
|
233
|
+
expect(outA.z).toBe(outB.z);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('randomPoisson', () => {
|
|
239
|
+
it('returns a non-negative integer', () => {
|
|
240
|
+
const random = rng();
|
|
241
|
+
for (let i = 0; i < 200; i++) {
|
|
242
|
+
const k = randomPoisson(random, 3);
|
|
243
|
+
expect(Number.isInteger(k)).toBe(true);
|
|
244
|
+
expect(k).toBeGreaterThanOrEqual(0);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
it('mean approximates lambda=1', () => {
|
|
248
|
+
const random = rng();
|
|
249
|
+
let sum = 0;
|
|
250
|
+
const n = 5000;
|
|
251
|
+
for (let i = 0; i < n; i++) sum += randomPoisson(random, 1);
|
|
252
|
+
expect(sum / n).toBeCloseTo(1, 0);
|
|
253
|
+
});
|
|
254
|
+
it('mean approximates lambda=5', () => {
|
|
255
|
+
const random = rng();
|
|
256
|
+
let sum = 0;
|
|
257
|
+
const n = 5000;
|
|
258
|
+
for (let i = 0; i < n; i++) sum += randomPoisson(random, 5);
|
|
259
|
+
expect(sum / n).toBeCloseTo(5, 0);
|
|
260
|
+
});
|
|
261
|
+
it('throws for lambda <= 0', () => {
|
|
262
|
+
expect(() => randomPoisson(rng(), 0)).toThrow(RangeError);
|
|
263
|
+
expect(() => randomPoisson(rng(), -1)).toThrow(RangeError);
|
|
264
|
+
});
|
|
265
|
+
it('is deterministic for the same seed', () => {
|
|
266
|
+
const a = rng();
|
|
267
|
+
const b = rng();
|
|
268
|
+
for (let i = 0; i < 20; i++) expect(randomPoisson(a, 3)).toBe(randomPoisson(b, 3));
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('randomWeighted', () => {
|
|
273
|
+
it('returns indices within the weights array', () => {
|
|
274
|
+
const random = rng();
|
|
275
|
+
const weights = [1, 2, 3];
|
|
276
|
+
for (let i = 0; i < 100; i++) {
|
|
277
|
+
const idx = randomWeighted(random, weights);
|
|
278
|
+
expect(idx).toBeGreaterThanOrEqual(0);
|
|
279
|
+
expect(idx).toBeLessThan(weights.length);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
it('returns -1 for an empty array', () => {
|
|
283
|
+
expect(randomWeighted(rng(), [])).toBe(-1);
|
|
284
|
+
});
|
|
285
|
+
it('returns -1 for an all-zero weight array', () => {
|
|
286
|
+
expect(randomWeighted(rng(), [0, 0, 0])).toBe(-1);
|
|
287
|
+
});
|
|
288
|
+
it('always returns the only non-zero index when all others are zero', () => {
|
|
289
|
+
const random = rng();
|
|
290
|
+
for (let i = 0; i < 20; i++) {
|
|
291
|
+
expect(randomWeighted(random, [0, 10, 0])).toBe(1);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
it('is deterministic for the same seed', () => {
|
|
295
|
+
const a = rng();
|
|
296
|
+
const b = rng();
|
|
297
|
+
const weights = [1, 2, 3, 4];
|
|
298
|
+
for (let i = 0; i < 20; i++) {
|
|
299
|
+
expect(randomWeighted(a, weights)).toBe(randomWeighted(b, weights));
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('shuffle', () => {
|
|
305
|
+
it('returns an array of the same length', () => {
|
|
306
|
+
const items = [1, 2, 3, 4, 5];
|
|
307
|
+
expect(shuffle(rng(), items)).toHaveLength(items.length);
|
|
308
|
+
});
|
|
309
|
+
it('contains all original elements', () => {
|
|
310
|
+
const items = [1, 2, 3, 4, 5];
|
|
311
|
+
const result = shuffle(rng(), items);
|
|
312
|
+
expect(result.sort()).toEqual(items.slice().sort());
|
|
313
|
+
});
|
|
314
|
+
it('does not mutate the original array', () => {
|
|
315
|
+
const items = [1, 2, 3, 4, 5];
|
|
316
|
+
const original = items.slice();
|
|
317
|
+
shuffle(rng(), items);
|
|
318
|
+
expect(items).toEqual(original);
|
|
319
|
+
});
|
|
320
|
+
it('is deterministic for the same seed', () => {
|
|
321
|
+
const items = [1, 2, 3, 4, 5, 6, 7, 8];
|
|
322
|
+
expect(shuffle(rng(), items)).toEqual(shuffle(rng(), items));
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('shuffleInPlace', () => {
|
|
327
|
+
it('retains all original elements', () => {
|
|
328
|
+
const items = [1, 2, 3, 4, 5];
|
|
329
|
+
const original = items.slice().sort();
|
|
330
|
+
shuffleInPlace(rng(), items);
|
|
331
|
+
expect(items.sort()).toEqual(original);
|
|
332
|
+
});
|
|
333
|
+
it('mutates the original array', () => {
|
|
334
|
+
const items = [1, 2, 3, 4, 5, 6, 7, 8];
|
|
335
|
+
const copy = items.slice();
|
|
336
|
+
shuffleInPlace(rng(), items);
|
|
337
|
+
// After sufficient elements it should have changed (very unlikely to match)
|
|
338
|
+
// — use a large array to make accidental match astronomically unlikely.
|
|
339
|
+
expect(items).toHaveLength(copy.length);
|
|
340
|
+
});
|
|
341
|
+
it('is deterministic for the same seed', () => {
|
|
342
|
+
const a = [1, 2, 3, 4, 5, 6, 7, 8];
|
|
343
|
+
const b = [1, 2, 3, 4, 5, 6, 7, 8];
|
|
344
|
+
shuffleInPlace(rng(), a);
|
|
345
|
+
shuffleInPlace(rng(), b);
|
|
346
|
+
expect(a).toEqual(b);
|
|
347
|
+
});
|
|
348
|
+
});
|