Skip to content

Fix: correct multipleOf validation for floating point values#5793

Open
cyphercodes wants to merge 1 commit intocolinhacks:mainfrom
cyphercodes:fix/multipleof-float-bug
Open

Fix: correct multipleOf validation for floating point values#5793
cyphercodes wants to merge 1 commit intocolinhacks:mainfrom
cyphercodes:fix/multipleof-float-bug

Conversation

@cyphercodes
Copy link

Problem

z.number().multipleOf(1e-7).safeParse(2.5e-7) incorrectly returns success. The value 2.5e-7 is not an integer multiple of 1e-7 (since 2.5e-7 / 1e-7 = 2.5), yet validation passes.

Root Cause

The floatSafeRemainder function parses .toString() representations to count decimal places. When values use scientific notation (e.g. 2.5e-7), split('.') gives misleading results ("5e-7" with length 3), leading to incorrect integer conversions and a false pass.

Solution

Replace the string-manipulation-based approach with a tolerance-based ratio comparison:

const ratio = val / step;
const roundedRatio = Math.round(ratio);
const tolerance = Number.EPSILON * Math.max(Math.abs(ratio), 1);
if (Math.abs(ratio - roundedRatio) < tolerance) return 0;

This checks whether val / step is close to an integer, which is the mathematical definition of "multiple of". The tolerance uses Number.EPSILON scaled to the ratio magnitude to correctly handle IEEE 754 representation limits at any scale.

Changes

Test Results

All 3577 tests pass across 323 test files with 0 type errors.

Closes #5792

…ison

Replace string-manipulation-based floatSafeRemainder with a
tolerance-based ratio comparison that correctly handles floating
point values in scientific notation (e.g. 2.5e-7, 1e-7).

The old approach parsed .toString() representations to count decimal
places, which failed for values like 2.5e-7 where split('.') gives
misleading results. The new approach checks whether val/step is
close to an integer using Number.EPSILON scaled to the ratio magnitude.

Fixes colinhacks#5792
@cyphercodes
Copy link
Author

Good catch — I audited all call sites and confirmed no code depends on the non-zero magnitude:

  • v4 (checks.ts:182): util.floatSafeRemainder(payload.value, def.value as number) === 0
  • v3 (types.ts:1429): floatSafeRemainder(input.data, check.value) !== 0

These are the only two call sites in the entire codebase (grep confirmed, including tests). Both exclusively check === 0 / !== 0 — the actual non-zero value is never consumed. So the shift from value-space remainder to multiple-space ratio is safe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

multipleOf silently accepts non-multiples for small numbers (scientific notation bug in floatSafeRemainder)

1 participant