Skip to content

Commit 95f098f

Browse files
committed
Fix floating point precision bug in input steppers
1 parent 681a379 commit 95f098f

File tree

2 files changed

+100
-2
lines changed

2 files changed

+100
-2
lines changed

components/dash-core-components/src/components/Input.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ function Input({
176176
(direction: 'increment' | 'decrement') => {
177177
const currentValue = parseFloat(input.current.value) || 0;
178178
const stepAsNum = parseFloat(step as string) || 1;
179+
180+
// Count decimal places to avoid floating point precision issues
181+
const decimalPlaces = (stepAsNum.toString().split('.')[1] || '')
182+
.length;
183+
179184
const newValue =
180185
direction === 'increment'
181186
? currentValue + stepAsNum
@@ -196,8 +201,13 @@ function Input({
196201
);
197202
}
198203

199-
input.current.value = constrainedValue.toString();
200-
setValue(constrainedValue.toString());
204+
// Round to the step's decimal precision
205+
const roundedValue = parseFloat(
206+
constrainedValue.toFixed(decimalPlaces)
207+
);
208+
209+
input.current.value = roundedValue.toString();
210+
setValue(roundedValue.toString());
201211
onEvent();
202212
},
203213
[step, props.min, props.max, onEvent]

components/dash-core-components/tests/integration/input/test_number_input.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import time
22
import sys
3+
import pytest
34
from dash import Dash, Input, Output, html, dcc
45
from selenium.webdriver.common.keys import Keys
56

@@ -148,6 +149,93 @@ def update_output(val):
148149
assert dash_dcc.get_logs() == []
149150

150151

152+
@pytest.mark.parametrize("step", [0.1, 0.01, 0.001, 0.0001])
153+
def test_inni006_stepper_floating_point_precision(dash_dcc, step):
154+
"""Test that stepper increments/decrements with decimal steps don't accumulate floating point errors."""
155+
156+
app = Dash(__name__)
157+
app.layout = html.Div(
158+
[
159+
dcc.Input(id="decimal-input", value=0, type="number", step=step),
160+
html.Div(id="output"),
161+
]
162+
)
163+
164+
@app.callback(Output("output", "children"), [Input("decimal-input", "value")])
165+
def update_output(val):
166+
return val
167+
168+
dash_dcc.start_server(app)
169+
increment_btn = dash_dcc.find_element(".dash-stepper-increment")
170+
decrement_btn = dash_dcc.find_element(".dash-stepper-decrement")
171+
172+
# Determine decimal places for formatting
173+
decimal_places = len(str(step).split(".")[1]) if "." in str(step) else 0
174+
num_clicks = 9
175+
176+
# Test increment: without precision fix, accumulates floating point errors (e.g., 0.30000000000000004)
177+
for i in range(1, num_clicks + 1):
178+
increment_btn.click()
179+
expected = format(step * i, f".{decimal_places}f")
180+
dash_dcc.wait_for_text_to_equal("#output", expected)
181+
182+
# Test decrement: should go back down through the same values
183+
for i in range(num_clicks - 1, 0, -1):
184+
decrement_btn.click()
185+
expected = format(step * i, f".{decimal_places}f")
186+
dash_dcc.wait_for_text_to_equal("#output", expected)
187+
188+
# One more decrement to get back to 0
189+
decrement_btn.click()
190+
dash_dcc.wait_for_text_to_equal("#output", "0")
191+
192+
assert dash_dcc.get_logs() == []
193+
194+
195+
@pytest.mark.parametrize("step", [0.00001, 0.000001])
196+
def test_inni007_stepper_very_small_steps(dash_dcc, step):
197+
"""Test that stepper works correctly with very small decimal steps."""
198+
199+
app = Dash(__name__)
200+
app.layout = html.Div(
201+
[
202+
dcc.Input(id="decimal-input", value=0, type="number", step=step),
203+
html.Div(id="output"),
204+
]
205+
)
206+
207+
@app.callback(Output("output", "children"), [Input("decimal-input", "value")])
208+
def update_output(val):
209+
return val
210+
211+
dash_dcc.start_server(app)
212+
increment_btn = dash_dcc.find_element(".dash-stepper-increment")
213+
decrement_btn = dash_dcc.find_element(".dash-stepper-decrement")
214+
215+
# For very small steps, format with enough precision then strip trailing zeros
216+
step_str = f"{step:.10f}".rstrip("0").rstrip(".")
217+
decimal_places = len(step_str.split(".")[1]) if "." in step_str else 0
218+
num_clicks = 5
219+
220+
# Test increment
221+
for i in range(1, num_clicks + 1):
222+
increment_btn.click()
223+
expected = f"{step * i:.{decimal_places}f}".rstrip("0").rstrip(".")
224+
dash_dcc.wait_for_text_to_equal("#output", expected)
225+
226+
# Test decrement
227+
for i in range(num_clicks - 1, 0, -1):
228+
decrement_btn.click()
229+
expected = f"{step * i:.{decimal_places}f}".rstrip("0").rstrip(".")
230+
dash_dcc.wait_for_text_to_equal("#output", expected)
231+
232+
# One more decrement to get back to 0
233+
decrement_btn.click()
234+
dash_dcc.wait_for_text_to_equal("#output", "0")
235+
236+
assert dash_dcc.get_logs() == []
237+
238+
151239
def test_inni010_valid_numbers(dash_dcc, ninput_app):
152240
dash_dcc.start_server(ninput_app)
153241
for num, op in (

0 commit comments

Comments
 (0)