-
Notifications
You must be signed in to change notification settings - Fork 157
feat(range): add floating-point precision snap, lerp, and logarithmic scale math #1038
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@solid-primitives/range": minor | ||
| --- | ||
|
|
||
| Add precision math helpers (`precisionRound`, `snapToStep`), linear normalization (`lerp`, `inverseLerp`), and logarithmic audio scale converters (`logScale`, `inverseLogScale`). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,34 @@ | ||
| export { type RangeProps } from "./common.js"; | ||
| export * from "./repeat.js"; | ||
| export * from "./mapRange.js"; | ||
| export * from "./indexRange.js"; | ||
| import { createMemo, type Accessor } from "solid-js"; | ||
| import { type MaybeAccessor, access } from "@solid-primitives/utils"; | ||
|
|
||
| export * from "./math.js"; | ||
|
|
||
| export function range(to: number): number[]; | ||
| export function range(from: number, to: number, step?: number): number[]; | ||
| export function range(from: number, to?: number, step: number = 1): number[] { | ||
| if (typeof to === "undefined") { | ||
| to = from; | ||
| from = 0; | ||
| } | ||
| return Array.from( | ||
| { length: Math.floor((to - from) / step) + 1 }, | ||
| (v, i) => from + i * step, | ||
| ); | ||
| } | ||
|
|
||
| export function createRange(to: MaybeAccessor<number>): Accessor<number[]>; | ||
| export function createRange( | ||
| from: MaybeAccessor<number>, | ||
| to: MaybeAccessor<number>, | ||
| step?: MaybeAccessor<number>, | ||
| ): Accessor<number[]>; | ||
| export function createRange( | ||
| from: MaybeAccessor<number>, | ||
| to?: MaybeAccessor<number>, | ||
| step: MaybeAccessor<number> = 1, | ||
| ): Accessor<number[]> { | ||
| if (typeof to === "undefined") { | ||
| return createMemo(() => range(access(from))); | ||
| } | ||
| return createMemo(() => range(access(from), access(to), access(step))); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| export function precisionRound(value: number, decimalPlaces = 10): number { | ||
| const factor = 10 ** decimalPlaces; | ||
| return Math.round((value + Number.EPSILON) * factor) / factor; | ||
| } | ||
|
|
||
| export function inverseLerp(min: number, max: number, value: number): number { | ||
| if (min === max) return 0; | ||
| const ratio = (value - min) / (max - min); | ||
| return Math.max(0, Math.min(1, ratio)); | ||
| } | ||
|
|
||
| export function lerp(min: number, max: number, t: number): number { | ||
| return min + (max - min) * Math.max(0, Math.min(1, t)); | ||
| } | ||
|
|
||
| export function snapToStep( | ||
| value: number, | ||
| step: number, | ||
| min = 0, | ||
| ): number { | ||
| if (step <= 0) return value; | ||
| const stepDecimals = (step.toString().split(".")[1] || "").length; | ||
| const steps = Math.round((value - min) / step); | ||
| const snapped = min + steps * step; | ||
| return precisionRound(snapped, Math.max(stepDecimals, 4)); | ||
|
Comment on lines
+22
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Handle steps written in scientific notation.
Derive decimal precision in a way that supports exponent notation, or avoid fixed decimal-place rounding. Add a regression test for this input. 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| export function logScale( | ||
| min: number, | ||
| max: number, | ||
| ratio: number, | ||
| ): number { | ||
| const safeMin = Math.max(min, 0.00001); | ||
| const safeRatio = Math.max(0, Math.min(1, ratio)); | ||
| const logMin = Math.log(safeMin); | ||
| const logMax = Math.log(max); | ||
| return Math.exp(logMin + safeRatio * (logMax - logMin)); | ||
| } | ||
|
|
||
| export function inverseLogScale( | ||
| min: number, | ||
| max: number, | ||
| value: number, | ||
| ): number { | ||
| const safeMin = Math.max(min, 0.00001); | ||
| const safeVal = Math.max(safeMin, Math.min(max, value)); | ||
| const logMin = Math.log(safeMin); | ||
| const logMax = Math.log(max); | ||
| return (Math.log(safeVal) - logMin) / (logMax - logMin); | ||
|
Comment on lines
+45
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Handle equal logarithmic bounds before division.
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { describe, it, expect } from "vitest"; | ||
| import { | ||
| precisionRound, | ||
| snapToStep, | ||
| inverseLerp, | ||
| lerp, | ||
| logScale, | ||
| inverseLogScale, | ||
| } from "../src/math"; | ||
|
|
||
| describe("Range & Numeric Precision Math", () => { | ||
| it("eliminates floating-point addition errors", () => { | ||
| const buggySum = 0.1 + 0.2; | ||
| expect(precisionRound(buggySum, 4)).toBe(0.3); | ||
| }); | ||
|
|
||
| it("snaps to fractional step boundaries cleanly", () => { | ||
| expect(snapToStep(0.28, 0.1, 0)).toBe(0.3); | ||
| expect(snapToStep(0.22, 0.1, 0)).toBe(0.2); | ||
| expect(snapToStep(1.234, 0.05, 1)).toBe(1.25); | ||
| }); | ||
|
|
||
| it("performs linear interpolation and inverse normalization", () => { | ||
| expect(lerp(100, 200, 0.5)).toBe(150); | ||
| expect(inverseLerp(100, 200, 150)).toBe(0.5); | ||
| }); | ||
|
|
||
| it("correctly maps logarithmic audio slider curves", () => { | ||
| const minHz = 20; | ||
| const maxHz = 20000; | ||
| const midpoint = logScale(minHz, maxHz, 0.5); | ||
| expect(Math.round(midpoint)).toBe(632); | ||
| expect(inverseLogScale(minHz, maxHz, midpoint)).toBeCloseTo(0.5, 4); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve inclusive endpoints for decimal steps.
range(0, 0.3, 0.1)evaluates(to - from) / stepas approximately2.9999999999999996. Line 14 floors that value to2, so the result is[0, 0.1, 0.2]and omits0.3.Use a floating-point tolerance when deriving the step count. Normalize a final point that is within that tolerance to
to. This also fixes the same result fromcreateRange.🤖 Prompt for AI Agents