feat(stdlib): new modules + eslint/tsconfig migration
- Add array/async/etc. modules and type tests; migrate to eslint flat config and composite tsconfig (vitest typecheck enabled). - Fix PubSub.emit to snapshot listeners before iterating (stable EventEmitter semantics; avoids invoking listeners added during the same emit).
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { clamp } from '.';
|
||||
|
||||
describe('clamp', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { inverseLerp, lerp } from '.';
|
||||
|
||||
describe('lerp', () => {
|
||||
@@ -26,6 +26,16 @@ describe('lerp', () => {
|
||||
const result = lerp(0, 10, 1.5);
|
||||
expect(result).toBe(15);
|
||||
});
|
||||
|
||||
it('interpolates from a non-zero start', () => {
|
||||
expect(lerp(10, 20, 0.5)).toBe(15);
|
||||
expect(lerp(-10, 10, 0.25)).toBe(-5);
|
||||
});
|
||||
|
||||
it('propagates NaN and Infinity', () => {
|
||||
expect(lerp(0, 10, Number.NaN)).toBeNaN();
|
||||
expect(lerp(0, Number.POSITIVE_INFINITY, 0.5)).toBe(Number.POSITIVE_INFINITY);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inverseLerp', () => {
|
||||
|
||||
@@ -43,4 +43,12 @@ describe('remap', () => {
|
||||
// input range is zero (should return output min)
|
||||
expect(remap(5, 0, 0, 0, 100)).toBe(0);
|
||||
});
|
||||
|
||||
it('handle a reversed (descending) input range', () => {
|
||||
// 2 is 80% of the way from 10 down to 0
|
||||
expect(remap(2, 10, 0, 0, 100)).toBe(80);
|
||||
// clamps to the interval regardless of orientation
|
||||
expect(remap(15, 10, 0, 0, 100)).toBe(0);
|
||||
expect(remap(-5, 10, 0, 0, 100)).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,9 @@ export function remap(value: number, in_min: number, in_max: number, out_min: nu
|
||||
if (in_min === in_max)
|
||||
return out_min;
|
||||
|
||||
const clampedValue = clamp(value, in_min, in_max);
|
||||
// Clamp to the interval's actual bounds so a reversed (descending) input range still works;
|
||||
// inverseLerp itself handles in_min > in_max correctly.
|
||||
const clampedValue = clamp(value, Math.min(in_min, in_max), Math.max(in_min, in_max));
|
||||
|
||||
return lerp(out_min, out_max, inverseLerp(in_min, in_max, clampedValue));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { clampBigInt } from '.';
|
||||
|
||||
describe('clampBigInt', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { inverseLerpBigInt, lerpBigInt } from '.';
|
||||
|
||||
const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
|
||||
@@ -28,6 +28,20 @@ describe('lerpBigInt', () => {
|
||||
const result = lerpBigInt(0n, 10n, 1.5);
|
||||
expect(result).toBe(15n);
|
||||
});
|
||||
|
||||
it('truncates the fractional part toward zero', () => {
|
||||
expect(lerpBigInt(0n, 10n, 0.29)).toBe(2n); // 2.9 -> 2
|
||||
expect(lerpBigInt(0n, -10n, 0.29)).toBe(-2n); // -2.9 -> -2 (toward zero, asymmetric)
|
||||
});
|
||||
|
||||
it('stays exact for very large bigint ranges', () => {
|
||||
expect(lerpBigInt(0n, 10n ** 30n, 0.5)).toBe(5n * 10n ** 29n);
|
||||
});
|
||||
|
||||
it('interpolates a reversed (start > end) range', () => {
|
||||
expect(lerpBigInt(10n, 0n, 0.5)).toBe(5n);
|
||||
expect(lerpBigInt(10n, 0n, 0.25)).toBe(8n); // 10 + (-10 * 0.25) = 7.5 -> 8? truncation
|
||||
});
|
||||
});
|
||||
|
||||
describe('inverseLerpBigInt', () => {
|
||||
@@ -61,23 +75,20 @@ describe('inverseLerpBigInt', () => {
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('handles the maximum safe integer correctly', () => {
|
||||
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);
|
||||
expect(result).toBe(1);
|
||||
it('returns 1 at the maximum safe integer', () => {
|
||||
expect(inverseLerpBigInt(0n, MAX_SAFE_INTEGER, MAX_SAFE_INTEGER)).toBe(1);
|
||||
});
|
||||
|
||||
it('handles values just above the maximum safe integer correctly', () => {
|
||||
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, 0n);
|
||||
expect(result).toBe(0);
|
||||
it('returns 0 at the start of a max-safe-integer range', () => {
|
||||
expect(inverseLerpBigInt(0n, MAX_SAFE_INTEGER, 0n)).toBe(0);
|
||||
});
|
||||
|
||||
it('handles values just below the maximum safe integer correctly', () => {
|
||||
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);
|
||||
expect(result).toBe(1);
|
||||
it('returns the midpoint of a max-safe-integer range', () => {
|
||||
// 6-decimal SCALE quantizes the result, so allow ~1e-6 tolerance.
|
||||
expect(inverseLerpBigInt(0n, MAX_SAFE_INTEGER, MAX_SAFE_INTEGER / 2n)).toBeCloseTo(0.5, 5);
|
||||
});
|
||||
|
||||
it('handles values just above the maximum safe integer correctly', () => {
|
||||
const result = inverseLerpBigInt(0n, 2n ** 128n, 2n ** 127n);
|
||||
expect(result).toBe(0.5);
|
||||
it('handles values far beyond 2^53', () => {
|
||||
expect(inverseLerpBigInt(0n, 2n ** 128n, 2n ** 127n)).toBe(0.5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { maxBigInt } from '.';
|
||||
|
||||
describe('maxBigInt', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { minBigInt } from '.';
|
||||
|
||||
describe('minBigInt', () => {
|
||||
|
||||
@@ -29,4 +29,14 @@ describe('remapBigInt', () => {
|
||||
// input range is zero (should return output min)
|
||||
expect(remapBigInt(5n, 0n, 0n, 0n, 100n)).toBe(0n);
|
||||
});
|
||||
|
||||
it('stay exact for large ranges (no number round-trip)', () => {
|
||||
// 1/3 of 10^30, computed entirely in BigInt — only truncation, no float precision loss
|
||||
expect(remapBigInt(1n, 0n, 3n, 0n, 10n ** 30n)).toBe(333333333333333333333333333333n);
|
||||
});
|
||||
|
||||
it('preserve precision well beyond 2^53', () => {
|
||||
const huge = 10n ** 40n;
|
||||
expect(remapBigInt(1n, 0n, 2n, 0n, huge)).toBe(huge / 2n);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { clampBigInt } from '../clampBigInt';
|
||||
import { inverseLerpBigInt, lerpBigInt } from '../lerpBigInt';
|
||||
|
||||
/**
|
||||
* @name remapBigInt
|
||||
@@ -21,5 +20,7 @@ export function remapBigInt(value: bigint, in_min: bigint, in_max: bigint, out_m
|
||||
|
||||
const clampedValue = clampBigInt(value, in_min, in_max);
|
||||
|
||||
return lerpBigInt(out_min, out_max, inverseLerpBigInt(in_min, in_max, clampedValue));
|
||||
// Stay entirely in BigInt — round-tripping through a JS number (as inverseLerpBigInt does)
|
||||
// quantizes to ~6 decimals and overflows precision past 2^53, defeating the point of BigInt.
|
||||
return out_min + ((clampedValue - in_min) * (out_max - out_min)) / (in_max - in_min);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user