test_better_matchers/
numeric.rs1use 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
18pub trait Float: sealed::Sealed + Copy + PartialOrd + fmt::Debug {
23 fn abs_diff(self, other: Self) -> Self;
25
26 fn float_is_nan(self) -> bool;
28
29 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
61struct 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 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#[must_use]
100pub fn close_to<F: Float>(value: F, tolerance: F) -> impl Matcher<F> {
101 CloseToMatcher { value, tolerance }
102}
103
104struct 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#[must_use]
139pub fn between<F: Float>(low: F, high: F) -> impl Matcher<F> {
140 BetweenMatcher { low, high }
141}
142
143struct 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#[must_use]
176pub fn is_nan<F: Float>() -> impl Matcher<F> {
177 IsNanMatcher
178}
179
180struct 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#[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 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 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}