Skip to main content

nautilus_indicators/
testing.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Common test related helper functions.
17
18/// Relative tolerance used by [`approx_equal`].
19///
20/// Indicator values span many orders of magnitude, so the admissible error
21/// scales with them.
22pub const DEFAULT_RELATIVE_TOLERANCE: f64 = 1e-9;
23
24/// Absolute tolerance used by [`approx_equal`].
25///
26/// Applies near zero, where a relative bound collapses to nothing.
27pub const DEFAULT_ABSOLUTE_TOLERANCE: f64 = 1e-15;
28
29/// Checks whether two floating-point numbers agree to the default tolerances.
30///
31/// Use this for calculated results. Invariants such as reset values, bounds or
32/// period-one passthrough are exact and should be asserted with `==`.
33///
34/// # Example
35///
36/// ```
37/// use nautilus_indicators::testing::approx_equal;
38///
39/// // At 1e6 the relative bound is 1e-3, which covers a gap of 1e-4.
40/// assert!(approx_equal(1e6 + 1e-4, 1e6));
41/// assert!(!approx_equal(1.0, 1.000_1));
42/// ```
43#[must_use]
44pub fn approx_equal(a: f64, b: f64) -> bool {
45    approx_equal_with(a, b, DEFAULT_RELATIVE_TOLERANCE, DEFAULT_ABSOLUTE_TOLERANCE)
46}
47
48/// Checks whether two floating-point numbers agree to the given tolerances.
49///
50/// Equal to within `absolute`, or within `relative` scaled by the larger
51/// magnitude. Infinities of the same sign compare equal; any NaN does not.
52///
53/// # Example
54///
55/// ```
56/// use nautilus_indicators::testing::approx_equal_with;
57///
58/// assert!(approx_equal_with(100.0, 100.000_001, 1e-7, 0.0));
59/// assert!(!approx_equal_with(100.0, 100.000_1, 1e-7, 0.0));
60/// ```
61#[must_use]
62pub fn approx_equal_with(a: f64, b: f64, relative: f64, absolute: f64) -> bool {
63    if a.is_nan() || b.is_nan() {
64        return false;
65    }
66
67    if a.is_infinite() || b.is_infinite() {
68        // Scaling a tolerance by an infinite magnitude swallows any gap.
69        return a.is_infinite() && b.is_infinite() && a.is_sign_positive() == b.is_sign_positive();
70    }
71    let difference = (a - b).abs();
72    difference <= absolute || difference <= relative * a.abs().max(b.abs())
73}
74
75/// Asserts that two floating-point numbers agree to the default tolerances.
76///
77/// Panics with both values and the absolute and relative errors.
78///
79/// # Panics
80///
81/// If the values differ by more than the default tolerances.
82#[track_caller]
83pub fn assert_approx_equal(actual: f64, expected: f64) {
84    assert!(
85        approx_equal(actual, expected),
86        "approx_equal failed\n  actual:   {actual:?}\n  expected: {expected:?}\n  \
87         absolute error: {:e}\n  relative error: {:e}",
88        (actual - expected).abs(),
89        (actual - expected).abs() / actual.abs().max(expected.abs()),
90    );
91}
92
93#[cfg(test)]
94mod tests {
95    use rstest::rstest;
96
97    use super::{approx_equal, approx_equal_with};
98
99    #[rstest]
100    fn approx_equal_scales_with_magnitude() {
101        // At 1e6 the relative bound is 1e-3, and f64 spacing there is ~1.2e-10.
102        assert!(approx_equal(1e6 + 1e-4, 1e6));
103        assert!(approx_equal(1e-4 + 1e-14, 1e-4));
104    }
105
106    #[rstest]
107    fn approx_equal_still_separates_real_differences() {
108        assert!(!approx_equal(1.0, 1.000_1));
109        assert!(!approx_equal(1e6, 1.001e6));
110    }
111
112    #[rstest]
113    fn approx_equal_handles_zero_and_non_finite() {
114        assert!(approx_equal(0.0, -0.0));
115        assert!(approx_equal(f64::INFINITY, f64::INFINITY));
116        assert!(!approx_equal(f64::INFINITY, f64::NEG_INFINITY));
117        assert!(!approx_equal(f64::NAN, f64::NAN));
118    }
119
120    #[rstest]
121    fn approx_equal_with_honours_explicit_tolerances() {
122        assert!(approx_equal_with(100.0, 100.000_001, 1e-7, 0.0));
123        assert!(!approx_equal_with(100.0, 100.000_1, 1e-7, 0.0));
124        assert!(approx_equal_with(0.0, 1e-13, 0.0, 1e-12));
125    }
126}