Skip to main content

test_better_matchers/
numeric.rs

1//! Numeric matchers for floating-point values: [`close_to`], [`between`],
2//! [`is_nan`], and [`is_finite`].
3//!
4//! These are generic over the [`Float`] trait, which is *sealed*: it is
5//! implemented for `f32` and `f64` and cannot be implemented downstream.
6
7use std::fmt;
8
9use crate::description::Description;
10use crate::matcher::{MatchResult, Matcher, Mismatch};
11
12mod sealed {
13    pub trait Sealed {}
14    impl Sealed for f32 {}
15    impl Sealed for f64 {}
16}
17
18/// A floating-point type the numeric matchers operate on.
19///
20/// Sealed: implemented for `f32` and `f64` only, so adding a method here is
21/// never a breaking change for downstream code.
22pub trait Float: sealed::Sealed + Copy + PartialOrd + fmt::Debug {
23    /// The absolute difference between `self` and `other`.
24    fn abs_diff(self, other: Self) -> Self;
25
26    /// Whether `self` is `NaN`.
27    fn float_is_nan(self) -> bool;
28
29    /// Whether `self` is neither infinite nor `NaN`.
30    fn float_is_finite(self) -> bool;
31}
32
33impl Float for f32 {
34    fn abs_diff(self, other: Self) -> Self {
35        (self - other).abs()
36    }
37
38    fn float_is_nan(self) -> bool {
39        self.is_nan()
40    }
41
42    fn float_is_finite(self) -> bool {
43        self.is_finite()
44    }
45}
46
47impl Float for f64 {
48    fn abs_diff(self, other: Self) -> Self {
49        (self - other).abs()
50    }
51
52    fn float_is_nan(self) -> bool {
53        self.is_nan()
54    }
55
56    fn float_is_finite(self) -> bool {
57        self.is_finite()
58    }
59}
60
61/// The matcher behind [`close_to`].
62struct CloseToMatcher<F> {
63    value: F,
64    tolerance: F,
65}
66
67impl<F: Float> Matcher<F> for CloseToMatcher<F> {
68    fn check(&self, actual: &F) -> MatchResult {
69        let diff = actual.abs_diff(self.value);
70        // A `NaN` actual makes `diff` `NaN`, and `NaN <= tolerance` is false,
71        // so `NaN` correctly fails to be close to anything.
72        if diff <= self.tolerance {
73            MatchResult::pass()
74        } else {
75            MatchResult::fail(Mismatch::new(
76                self.description(),
77                format!("{actual:?} (off by {diff:?})"),
78            ))
79        }
80    }
81
82    fn description(&self) -> Description {
83        Description::text(format!("within {:?} of {:?}", self.tolerance, self.value))
84    }
85}
86
87/// Matches a float within `tolerance` of `value` (the comparison is
88/// `|actual - value| <= tolerance`).
89///
90/// ```
91/// use test_better_core::TestResult;
92/// use test_better_matchers::{close_to, expect};
93///
94/// fn main() -> TestResult {
95///     expect!(0.1_f64 + 0.2).to(close_to(0.3, 1e-9))?;
96///     Ok(())
97/// }
98/// ```
99#[must_use]
100pub fn close_to<F: Float>(value: F, tolerance: F) -> impl Matcher<F> {
101    CloseToMatcher { value, tolerance }
102}
103
104/// The matcher behind [`between`].
105struct BetweenMatcher<F> {
106    low: F,
107    high: F,
108}
109
110impl<F: Float> Matcher<F> for BetweenMatcher<F> {
111    fn check(&self, actual: &F) -> MatchResult {
112        if self.low <= *actual && *actual <= self.high {
113            MatchResult::pass()
114        } else {
115            MatchResult::fail(Mismatch::new(self.description(), format!("{actual:?}")))
116        }
117    }
118
119    fn description(&self) -> Description {
120        Description::text(format!(
121            "between {:?} and {:?} (inclusive)",
122            self.low, self.high
123        ))
124    }
125}
126
127/// Matches a float in the inclusive range `low..=high`.
128///
129/// ```
130/// use test_better_core::TestResult;
131/// use test_better_matchers::{between, expect};
132///
133/// fn main() -> TestResult {
134///     expect!(2.5_f64).to(between(0.0, 5.0))?;
135///     Ok(())
136/// }
137/// ```
138#[must_use]
139pub fn between<F: Float>(low: F, high: F) -> impl Matcher<F> {
140    BetweenMatcher { low, high }
141}
142
143/// The matcher behind [`is_nan`].
144struct IsNanMatcher;
145
146impl<F: Float> Matcher<F> for IsNanMatcher {
147    fn check(&self, actual: &F) -> MatchResult {
148        if actual.float_is_nan() {
149            MatchResult::pass()
150        } else {
151            MatchResult::fail(Mismatch::new(
152                Description::text("NaN"),
153                format!("{actual:?}"),
154            ))
155        }
156    }
157
158    fn description(&self) -> Description {
159        Description::text("NaN")
160    }
161}
162
163/// Matches a float that is `NaN`.
164///
165/// ```
166/// use test_better_core::TestResult;
167/// use test_better_matchers::{expect, is_nan};
168///
169/// fn main() -> TestResult {
170///     expect!(f64::NAN).to(is_nan())?;
171///     expect!(1.0_f64).to_not(is_nan())?;
172///     Ok(())
173/// }
174/// ```
175#[must_use]
176pub fn is_nan<F: Float>() -> impl Matcher<F> {
177    IsNanMatcher
178}
179
180/// The matcher behind [`is_finite`].
181struct IsFiniteMatcher;
182
183impl<F: Float> Matcher<F> for IsFiniteMatcher {
184    fn check(&self, actual: &F) -> MatchResult {
185        if actual.float_is_finite() {
186            MatchResult::pass()
187        } else {
188            MatchResult::fail(Mismatch::new(
189                Description::text("a finite number"),
190                format!("{actual:?}"),
191            ))
192        }
193    }
194
195    fn description(&self) -> Description {
196        Description::text("a finite number")
197    }
198}
199
200/// Matches a float that is finite (neither infinite nor `NaN`).
201///
202/// ```
203/// use test_better_core::TestResult;
204/// use test_better_matchers::{expect, is_finite};
205///
206/// fn main() -> TestResult {
207///     expect!(1.5_f64).to(is_finite())?;
208///     expect!(f64::INFINITY).to_not(is_finite())?;
209///     Ok(())
210/// }
211/// ```
212#[must_use]
213pub fn is_finite<F: Float>() -> impl Matcher<F> {
214    IsFiniteMatcher
215}
216
217#[cfg(test)]
218mod tests {
219    use test_better_core::{OrFail, TestResult};
220
221    use super::*;
222    use crate::{eq, expect, is_false, is_true};
223
224    #[test]
225    fn close_to_respects_the_tolerance() -> TestResult {
226        expect!(close_to(0.3, 1e-9).check(&(0.1_f64 + 0.2)).matched).to(is_true())?;
227        expect!(close_to(0.3_f64, 1e-9).check(&0.4).matched).to(is_false())?;
228        // The tolerance is the boundary, inclusive.
229        expect!(close_to(1.0_f64, 0.5).check(&1.5).matched).to(is_true())?;
230        expect!(close_to(1.0_f64, 0.5).check(&1.6).matched).to(is_false())?;
231        Ok(())
232    }
233
234    #[test]
235    fn close_to_failure_shows_the_tolerance_and_the_difference() -> TestResult {
236        let failure = close_to(1.0_f64, 0.1)
237            .check(&2.0)
238            .failure
239            .or_fail_with("2.0 is not within 0.1 of 1.0")?;
240        expect!(failure.expected.to_string()).to(eq("within 0.1 of 1.0".to_string()))?;
241        expect!(failure.actual.contains("off by")).to(is_true())?;
242        Ok(())
243    }
244
245    #[test]
246    fn between_is_an_inclusive_range() -> TestResult {
247        expect!(between(0.0_f64, 5.0).check(&0.0).matched).to(is_true())?;
248        expect!(between(0.0_f64, 5.0).check(&5.0).matched).to(is_true())?;
249        expect!(between(0.0_f64, 5.0).check(&5.1).matched).to(is_false())?;
250        expect!(between(0.0_f64, 5.0).check(&-0.1).matched).to(is_false())?;
251        Ok(())
252    }
253
254    #[test]
255    fn is_nan_matches_only_nan() -> TestResult {
256        expect!(is_nan().check(&f64::NAN).matched).to(is_true())?;
257        expect!(is_nan().check(&1.0_f64).matched).to(is_false())?;
258        // A `NaN` is never close to anything, including itself.
259        expect!(close_to(f64::NAN, 1.0).check(&f64::NAN).matched).to(is_false())?;
260        Ok(())
261    }
262
263    #[test]
264    fn is_finite_rejects_infinities_and_nan() -> TestResult {
265        expect!(is_finite().check(&1.5_f64).matched).to(is_true())?;
266        expect!(is_finite().check(&f64::INFINITY).matched).to(is_false())?;
267        expect!(is_finite().check(&f64::NEG_INFINITY).matched).to(is_false())?;
268        expect!(is_finite().check(&f64::NAN).matched).to(is_false())?;
269        Ok(())
270    }
271
272    #[test]
273    fn numeric_matchers_work_for_f32_too() -> TestResult {
274        expect!(close_to(1.0_f32, 0.01).check(&1.005).matched).to(is_true())?;
275        expect!(between(0.0_f32, 1.0).check(&0.5).matched).to(is_true())?;
276        expect!(is_nan().check(&f32::NAN).matched).to(is_true())?;
277        Ok(())
278    }
279}