diff --git a/src/validators/float.rs b/src/validators/float.rs index 3a79de9fe..8f79ebbee 100644 --- a/src/validators/float.rs +++ b/src/validators/float.rs @@ -108,9 +108,23 @@ 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 { + const EPSILON_FACTOR: f64 = 100.0; + let epsilon_threshold = f64::EPSILON * EPSILON_FACTOR; + + // Round the result of dividing the input value by the multiple + let rounded = (float / multiple_of).round(); + + // Calculate the difference between the rounded value and the original value + let diff = (float - rounded * multiple_of).abs(); + + // Calculate the relative error (avoid division by zero) + let relative_error = if float != 0.0 { diff / float.abs() } else { 0.0 }; + + // Threshold (considering both relative and absolute error) + let threshold = epsilon_threshold.max(multiple_of * epsilon_threshold); + + // Check if the difference exceeds the threshold and the relative error is significant + if diff > threshold && relative_error > epsilon_threshold { 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 931fe6377..9a8d05f74 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 c397f453c..2b2eaaded 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, )