From ed15f6cae1f7073868372e3ac8f54f67c974ca6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=95=82?= <51281148+K-dash@users.noreply.github.com> Date: Wed, 5 Feb 2025 22:42:14 +0900 Subject: [PATCH] Fix float `multiple_of` validation for negative numbers (#1373) --- src/validators/float.rs | 7 +++--- tests/validators/test_decimal.py | 40 ++++++++++++++++++++++++-------- tests/validators/test_float.py | 40 ++++++++++++++++++++++++-------- 3 files changed, 64 insertions(+), 23 deletions(-) diff --git a/src/validators/float.rs b/src/validators/float.rs index 30d3f3aa9..c2120d96b 100644 --- a/src/validators/float.rs +++ b/src/validators/float.rs @@ -109,9 +109,10 @@ impl Validator for ConstrainedFloatValidator { return Err(ValError::new(ErrorTypeDefaults::FiniteNumber, input)); } if let Some(multiple_of) = self.multiple_of { - let rem = float % multiple_of; - let threshold = float.abs() / 1e9; - if rem.abs() > threshold && (rem - multiple_of).abs() > threshold { + let tolerance = 1e-9; + let rounded_div = (float / multiple_of).round(); + let diff = (float - (rounded_div * multiple_of)).abs(); + if diff > tolerance { return Err(ValError::new( ErrorType::MultipleOf { multiple_of: multiple_of.into(), diff --git a/tests/validators/test_decimal.py b/tests/validators/test_decimal.py index fa1c0270d..0f165d5a8 100644 --- a/tests/validators/test_decimal.py +++ b/tests/validators/test_decimal.py @@ -171,24 +171,44 @@ def test_decimal_kwargs(py_and_json: PyAndJson, kwargs: dict[str, Any], input_va @pytest.mark.parametrize( 'multiple_of,input_value,error', [ - (0.5, 0.5, None), - (0.5, 1, None), + # Test cases for multiples of 0.5 + *[(0.5, round(i * 0.5, 1), None) for i in range(-4, 5)], + (0.5, 0.49, Err('Input should be a multiple of 0.5')), (0.5, 0.6, Err('Input should be a multiple of 0.5')), - (0.5, 0.51, Err('Input should be a multiple of 0.5')), + (0.5, -0.75, Err('Input should be a multiple of 0.5')), (0.5, 0.501, Err('Input should be a multiple of 0.5')), (0.5, 1_000_000.5, None), (0.5, 1_000_000.49, Err('Input should be a multiple of 0.5')), + (0.5, int(5e10), None), + # Test cases for multiples of 0.1 + *[(0.1, round(i * 0.1, 1), None) for i in range(-10, 11)], (0.1, 0, None), - (0.1, 0.0, None), - (0.1, 0.2, None), - (0.1, 0.3, None), - (0.1, 0.4, None), - (0.1, 0.5, None), (0.1, 0.5001, Err('Input should be a multiple of 0.1')), + (0.1, 0.05, Err('Input should be a multiple of 0.1')), + (0.1, -0.15, Err('Input should be a multiple of 0.1')), + (0.1, 1_000_000.1, None), + (0.1, 1_000_000.05, Err('Input should be a multiple of 0.1')), (0.1, 1, None), - (0.1, 1.0, None), (0.1, int(5e10), None), - (2.0, -2.0, None), + # Test cases for multiples of 2.0 + *[(2.0, i * 2.0, None) for i in range(-5, 6)], + (2.0, -2.1, Err('Input should be a multiple of 2')), + (2.0, -3.0, Err('Input should be a multiple of 2')), + (2.0, 1_000_002.0, None), + (2.0, 1_000_001.0, Err('Input should be a multiple of 2')), + (2.0, int(5e10), None), + # Test cases for multiples of 0.01 + *[(0.01, round(i * 0.01, 2), None) for i in range(-10, 11)], + (0.01, 0.005, Err('Input should be a multiple of 0.01')), + (0.01, -0.015, Err('Input should be a multiple of 0.01')), + (0.01, 1_000_000.01, None), + (0.01, 1_000_000.005, Err('Input should be a multiple of 0.01')), + (0.01, int(5e10), None), + # Test cases for values very close to zero + (0.1, 0.00001, Err('Input should be a multiple of 0.1')), + (0.1, -0.00001, Err('Input should be a multiple of 0.1')), + (0.01, 0.00001, Err('Input should be a multiple of 0.01')), + (0.01, -0.00001, Err('Input should be a multiple of 0.01')), ], ids=repr, ) diff --git a/tests/validators/test_float.py b/tests/validators/test_float.py index c572fc68b..1269a6d51 100644 --- a/tests/validators/test_float.py +++ b/tests/validators/test_float.py @@ -105,24 +105,44 @@ def test_float_kwargs(py_and_json: PyAndJson, kwargs: dict[str, Any], input_valu @pytest.mark.parametrize( 'multiple_of,input_value,error', [ - (0.5, 0.5, None), - (0.5, 1, None), + # Test cases for multiples of 0.5 + *[(0.5, round(i * 0.5, 1), None) for i in range(-4, 5)], + (0.5, 0.49, Err('Input should be a multiple of 0.5')), (0.5, 0.6, Err('Input should be a multiple of 0.5')), - (0.5, 0.51, Err('Input should be a multiple of 0.5')), + (0.5, -0.75, Err('Input should be a multiple of 0.5')), (0.5, 0.501, Err('Input should be a multiple of 0.5')), (0.5, 1_000_000.5, None), (0.5, 1_000_000.49, Err('Input should be a multiple of 0.5')), + (0.5, int(5e10), None), + # Test cases for multiples of 0.1 + *[(0.1, round(i * 0.1, 1), None) for i in range(-10, 11)], (0.1, 0, None), - (0.1, 0.0, None), - (0.1, 0.2, None), - (0.1, 0.3, None), - (0.1, 0.4, None), - (0.1, 0.5, None), (0.1, 0.5001, Err('Input should be a multiple of 0.1')), + (0.1, 0.05, Err('Input should be a multiple of 0.1')), + (0.1, -0.15, Err('Input should be a multiple of 0.1')), + (0.1, 1_000_000.1, None), + (0.1, 1_000_000.05, Err('Input should be a multiple of 0.1')), (0.1, 1, None), - (0.1, 1.0, None), (0.1, int(5e10), None), - (2.0, -2.0, None), + # Test cases for multiples of 2.0 + *[(2.0, i * 2.0, None) for i in range(-5, 6)], + (2.0, -2.1, Err('Input should be a multiple of 2')), + (2.0, -3.0, Err('Input should be a multiple of 2')), + (2.0, 1_000_002.0, None), + (2.0, 1_000_001.0, Err('Input should be a multiple of 2')), + (2.0, int(5e10), None), + # Test cases for multiples of 0.01 + *[(0.01, round(i * 0.01, 2), None) for i in range(-10, 11)], + (0.01, 0.005, Err('Input should be a multiple of 0.01')), + (0.01, -0.015, Err('Input should be a multiple of 0.01')), + (0.01, 1_000_000.01, None), + (0.01, 1_000_000.005, Err('Input should be a multiple of 0.01')), + (0.01, int(5e10), None), + # Test cases for values very close to zero + (0.1, 0.00001, Err('Input should be a multiple of 0.1')), + (0.1, -0.00001, Err('Input should be a multiple of 0.1')), + (0.01, 0.00001, Err('Input should be a multiple of 0.01')), + (0.01, -0.00001, Err('Input should be a multiple of 0.01')), ], ids=repr, )