diff --git a/src/utils.js b/src/utils.js index 5c3557865..54ae1c6dd 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,8 @@ import { findDOMNode } from 'react-dom'; import keyCode from 'rc-util/lib/KeyCode'; +const MAX_PRECISION_FOR_OPERATIONS = 15; + export function isEventFromHandle(e, handles) { try { return Object.keys(handles) @@ -19,28 +21,51 @@ export function isNotTouchEvent(e) { (e.type.toLowerCase() === 'touchend' && e.touches.length > 0); } +export function getPrecision(step) { + const stepString = step.toString(); + let precision = 0; + if (stepString.indexOf('.') >= 0) { + precision = stepString.length - stepString.indexOf('.') - 1; + } + return precision; +} + +function withPrecision(value, precision) { + return parseFloat(value.toFixed(precision)); +} + +// safeDivideBy and safeMultiply: if either term is a float, +// then round the result to the combined precision + +function safeDivideBy(a, b) { + const precision = Math.min( + getPrecision(a) + getPrecision(b), + MAX_PRECISION_FOR_OPERATIONS + ); + return precision === 0 ? a / b : withPrecision(a / b, precision); +} + +function safeMultiply(a, b) { + const precision = Math.min( + getPrecision(a) + getPrecision(b), + MAX_PRECISION_FOR_OPERATIONS + ); + return withPrecision(a * b, precision); +} + export function getClosestPoint(val, { marks, step, min, max }) { const points = Object.keys(marks).map(parseFloat); if (step !== null) { - const maxSteps = Math.floor((max - min) / step); - const steps = Math.min((val - min) / step, maxSteps); + const maxSteps = Math.floor(safeDivideBy(max - min, step)); + const steps = Math.min(safeDivideBy(val - min, step), maxSteps); const closestStep = - Math.round(steps) * step + min; + safeMultiply(Math.round(steps), step) + min; points.push(closestStep); } const diffs = points.map(point => Math.abs(val - point)); return points[diffs.indexOf(Math.min(...diffs))]; } -export function getPrecision(step) { - const stepString = step.toString(); - let precision = 0; - if (stepString.indexOf('.') >= 0) { - precision = stepString.length - stepString.indexOf('.') - 1; - } - return precision; -} - export function getMousePosition(vertical, e) { return vertical ? e.clientY : e.pageX; } @@ -70,7 +95,7 @@ export function ensureValuePrecision(val, props) { const { step } = props; const closestPoint = isFinite(getClosestPoint(val, props)) ? getClosestPoint(val, props) : 0; // eslint-disable-line return step === null ? closestPoint : - parseFloat(closestPoint.toFixed(getPrecision(step))); + withPrecision(closestPoint, getPrecision(step)); } export function pauseEvent(e) { diff --git a/tests/utils.test.js b/tests/utils.test.js index 4e8381f01..843833041 100644 --- a/tests/utils.test.js +++ b/tests/utils.test.js @@ -38,5 +38,17 @@ describe('utils', () => { expect(utils.getClosestPoint(value, props)).toBe(96); }); + + it('should properly handle floating point arithmetic', () => { + const value = 5.3; + const props = { + marks: {}, + step: 0.05, + min: 0, + max: 5.3 + }; + + expect(utils.getClosestPoint(value, props)).toBe(5.3); + }); }); });