import { clamp } from "../../internals/slider-utils/clamp.ts"; import { getFormatter } from "./format-number.ts"; const STEP_EPSILON_FACTOR = 1e-10; function getFractionDigits(format?: Intl.NumberFormatOptions): { maximumFractionDigits: number; minimumFractionDigits: number; } { const defaultOptions = getFormatter("en-US").resolvedOptions(); const minimumFractionDigits = format?.minimumFractionDigits ?? defaultOptions.minimumFractionDigits ?? 0; const maximumFractionDigits = Math.max( format?.maximumFractionDigits ?? defaultOptions.maximumFractionDigits ?? 20, minimumFractionDigits, ); return { maximumFractionDigits, minimumFractionDigits }; } function roundToFractionDigits( value: number, maximumFractionDigits: number, ): number { if (!Number.isFinite(value)) { return value; } const digits = Math.min(Math.max(maximumFractionDigits, 0), 20); return Number(value.toFixed(digits)); } function removeFloatingPointErrors( value: number, format?: Intl.NumberFormatOptions, ): number { const { maximumFractionDigits } = getFractionDigits(format); return roundToFractionDigits(value, maximumFractionDigits); } function snapToStep( clampedValue: number, base: number, step: number, mode: "directional" | "nearest" = "directional", ): number { if (step === 0) { return clampedValue; } const stepSize = Math.abs(step); const direction = Math.sign(step); const tolerance = stepSize * STEP_EPSILON_FACTOR * direction; const divisor = mode === "nearest" ? step : stepSize; const rawSteps = (clampedValue - base + tolerance) / divisor; let snappedSteps: number; if (mode === "nearest") { snappedSteps = Math.round(rawSteps); } else if (direction > 0) { snappedSteps = Math.floor(rawSteps); } else { snappedSteps = Math.ceil(rawSteps); } const stepForResult = mode === "nearest" ? step : stepSize; return base + snappedSteps * stepForResult; } export function toValidatedNumber( value: number | null, params: { step: number | undefined; minWithDefault: number; maxWithDefault: number; minWithZeroDefault: number; format: Intl.NumberFormatOptions | undefined; snapOnStep: boolean; small: boolean; clamp: boolean; }, ): number | null { if (value === null) { return value; } const { step, minWithDefault, maxWithDefault, minWithZeroDefault, format, snapOnStep, small, clamp: shouldClamp, } = params; const clampedValue = shouldClamp ? clamp(value, minWithDefault, maxWithDefault) : value; if (step != null && snapOnStep) { if (step === 0) { return removeFloatingPointErrors(clampedValue, format); } let base = minWithZeroDefault; if (!small && minWithDefault !== Number.MIN_SAFE_INTEGER) { base = minWithDefault; } const snappedValue = snapToStep( clampedValue, base, step, small ? "nearest" : "directional", ); return removeFloatingPointErrors(snappedValue, format); } return removeFloatingPointErrors(clampedValue, format); }