precision_core/
tolerance.rs

1//! Tolerance-based comparison operations.
2
3use crate::decimal::Decimal;
4
5/// Compares two decimals with an absolute tolerance.
6///
7/// Returns `true` if `|a - b| <= tolerance`.
8#[must_use]
9pub fn approx_eq(a: Decimal, b: Decimal, tolerance: Decimal) -> bool {
10    let diff = if a >= b { a - b } else { b - a };
11    diff <= tolerance
12}
13
14/// Compares two decimals with a relative tolerance.
15///
16/// Returns `true` if `|a - b| <= max(|a|, |b|) * relative_tolerance`.
17///
18/// For comparing values near zero, use `approx_eq` with an absolute tolerance instead.
19#[must_use]
20pub fn approx_eq_relative(a: Decimal, b: Decimal, relative_tolerance: Decimal) -> bool {
21    if a == b {
22        return true;
23    }
24
25    let diff = (a - b).abs();
26    let max_abs = a.abs().max(b.abs());
27
28    if max_abs.is_zero() {
29        return diff.is_zero();
30    }
31
32    if let Some(threshold) = max_abs.checked_mul(relative_tolerance) {
33        diff <= threshold
34    } else {
35        false
36    }
37}
38
39/// Compares two decimals with both absolute and relative tolerances.
40///
41/// Returns `true` if either tolerance check passes.
42/// This handles both small values (where absolute tolerance matters)
43/// and large values (where relative tolerance matters).
44#[must_use]
45pub fn approx_eq_ulps(
46    a: Decimal,
47    b: Decimal,
48    absolute_tolerance: Decimal,
49    relative_tolerance: Decimal,
50) -> bool {
51    approx_eq(a, b, absolute_tolerance) || approx_eq_relative(a, b, relative_tolerance)
52}
53
54/// Checks if a value is within a percentage of another value.
55///
56/// Returns `true` if `|a - b| <= |b| * (percentage / 100)`.
57#[must_use]
58pub fn within_percentage(a: Decimal, b: Decimal, percentage: Decimal) -> bool {
59    if b.is_zero() {
60        return a.is_zero();
61    }
62
63    let diff = (a - b).abs();
64    let threshold = b
65        .abs()
66        .checked_mul(percentage)
67        .and_then(|v| v.checked_div(Decimal::ONE_HUNDRED));
68
69    match threshold {
70        Some(t) => diff <= t,
71        None => false,
72    }
73}
74
75/// Checks if a value is within a basis point tolerance of another value.
76///
77/// One basis point = 0.01% = 0.0001.
78#[must_use]
79pub fn within_basis_points(a: Decimal, b: Decimal, bps: Decimal) -> bool {
80    if b.is_zero() {
81        return a.is_zero();
82    }
83
84    let diff = (a - b).abs();
85    let threshold = b
86        .abs()
87        .checked_mul(bps)
88        .and_then(|v| v.checked_div(Decimal::new(10000, 0)));
89
90    match threshold {
91        Some(t) => diff <= t,
92        None => false,
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn approx_eq_exact() {
102        let a = Decimal::new(100, 2);
103        let tolerance = Decimal::new(1, 4);
104        assert!(approx_eq(a, a, tolerance));
105    }
106
107    #[test]
108    fn approx_eq_within_tolerance() {
109        let a = Decimal::new(1000, 3);
110        let b = Decimal::new(1001, 3);
111        let tolerance = Decimal::new(1, 3);
112        assert!(approx_eq(a, b, tolerance));
113    }
114
115    #[test]
116    fn approx_eq_outside_tolerance() {
117        let a = Decimal::new(1000, 3);
118        let b = Decimal::new(1002, 3);
119        let tolerance = Decimal::new(1, 3);
120        assert!(!approx_eq(a, b, tolerance));
121    }
122
123    #[test]
124    fn approx_eq_relative_basic() {
125        let a = Decimal::from(100i64);
126        let b = Decimal::from(101i64);
127        let tolerance = Decimal::new(2, 2); // 2%
128        assert!(approx_eq_relative(a, b, tolerance));
129    }
130
131    #[test]
132    fn approx_eq_relative_large_values() {
133        let a = Decimal::from(1_000_000i64);
134        let b = Decimal::from(1_000_100i64);
135        let tolerance = Decimal::new(1, 3); // 0.1%
136        assert!(approx_eq_relative(a, b, tolerance));
137    }
138
139    #[test]
140    fn within_percentage_basic() {
141        let a = Decimal::from(102i64);
142        let b = Decimal::from(100i64);
143        assert!(within_percentage(a, b, Decimal::from(5i64))); // within 5%
144        assert!(!within_percentage(a, b, Decimal::from(1i64))); // not within 1%
145    }
146
147    #[test]
148    fn within_basis_points_basic() {
149        let a = Decimal::new(10010, 2); // 100.10
150        let b = Decimal::from(100i64);
151        // 100 bps of 100 = 1.0, difference is 0.10, so within 100 bps
152        assert!(within_basis_points(a, b, Decimal::from(100i64))); // within 100 bps (1%)
153        // 5 bps of 100 = 0.05, difference is 0.10, so NOT within 5 bps
154        assert!(!within_basis_points(a, b, Decimal::from(5i64))); // not within 5 bps
155    }
156
157    #[test]
158    fn within_basis_points_zero() {
159        let a = Decimal::from(100i64);
160        let b = Decimal::ZERO;
161        assert!(!within_basis_points(a, b, Decimal::from(100i64)));
162        assert!(within_basis_points(Decimal::ZERO, b, Decimal::from(100i64)));
163    }
164}