assert_within/
lib.rs

1//! Provides a macro `assert_within!` for tests involving floating point numbers.
2//!
3//! ```rust ignore
4//! assert_within!(+0.001, val, target, "Value was not within additive 0.001: {more} {context}");
5//! assert_within!(~0.05, val, target, "Value was not within 5% of target: {additional} {information:?}");
6//! ```
7//!
8//! Highlights include:
9//!
10//! * Pass arguments by reference or value
11//! * Sigils (+, ~) indicate additive or relative error
12//! * Traps Nan in any of the arguments
13//! * Errors cause both the stringified expressions and their values to be displayed
14//! * Arbitrary additional format args
15//! * Generic over `num_traits::FloatCore`
16//! * no_std compatible
17
18#![no_std]
19
20use core::{
21    borrow::Borrow,
22    fmt::{self, Display},
23};
24use num_traits::float::FloatCore;
25
26/// Helper for asserting that an f64 value is within +/- epsilon of another, additively
27#[doc(hidden)]
28#[allow(clippy::too_many_arguments)]
29pub fn assert_within_add_impl<N: Display + FloatCore>(
30    file: &'static str,
31    line: u32,
32    val: impl Borrow<N>,
33    val_str: &'static str,
34    target: impl Borrow<N>,
35    target_str: &'static str,
36    eps: impl Borrow<N>,
37    context: fmt::Arguments,
38) {
39    let val = val.borrow();
40    let target = target.borrow();
41    let eps = eps.borrow();
42
43    if eps.is_nan() {
44        panic!("assert_within failed at {file}:{line}:\nepsilon was Nan: {eps}\n{context}");
45    }
46
47    if *eps < N::zero() {
48        panic!(
49            "assert_within failed at {file}:{line}:\nEpsilon cannot be negative when used with assert_within! macro: {eps}\n{context}"
50        )
51    }
52
53    if val.is_nan() {
54        panic!("assert_within failed at {file}:{line}:\n`{val_str}` was Nan: {val}\n{context}");
55    }
56
57    if target.is_nan() {
58        panic!(
59            "assert_within failed at {file}:{line}:\n`{target_str}` was Nan: {target}\n{context}"
60        );
61    }
62
63    if *val < *target - *eps {
64        panic!(
65            "assert_within failed at {file}:{line}:\n`{val_str}` was less than `{target_str}` - {eps})\nleft:  {val}\nright: {target}\n{context}"
66        );
67    }
68
69    if *val > *target + *eps {
70        panic!(
71            "assert_within failed at {file}:{line}:\n`{val_str}` was greater than `{target_str}` + {eps})\nleft:  {val}\nright: {target}\n{context}"
72        );
73    }
74}
75
76/// Helper for asserting that an f64 value is within +/- epsilon of another, multiplicatively
77#[doc(hidden)]
78#[allow(clippy::too_many_arguments)]
79pub fn assert_within_mul_impl<N: Display + FloatCore>(
80    file: &'static str,
81    line: u32,
82    val: impl Borrow<N>,
83    val_str: &'static str,
84    target: impl Borrow<N>,
85    target_str: &'static str,
86    eps: impl Borrow<N>,
87    context: fmt::Arguments,
88) {
89    let val = val.borrow();
90    let target = target.borrow();
91    let eps = eps.borrow();
92
93    if eps.is_nan() {
94        panic!("assert_within failed at {file}:{line}:\nepsilon was Nan: {eps}\n{context}");
95    }
96
97    if *eps < N::zero() {
98        panic!(
99            "assert_within failed at {file}:{line}:\nEpsilon cannot be negative when used with assert_within! macro: {eps}\n{context}"
100        )
101    }
102
103    if val.is_nan() {
104        panic!("assert_within failed at {file}:{line}:\n`{val_str}` was Nan: {val}\n{context}");
105    }
106
107    if target.is_nan() {
108        panic!(
109            "assert_within failed at {file}:{line}:\n`{target_str}` was Nan: {target}\n{context}"
110        );
111    }
112
113    let one_minus_eps = N::one() - *eps;
114    let one_plus_eps = N::one() + *eps;
115    if target.is_sign_positive() {
116        if *val < one_minus_eps * *target {
117            panic!(
118                "assert_within failed at {file}:{line}:\n`{val_str}` was less than (1 ± {eps}) * `{target_str}`\nleft:  {val}\nright: {target}\n{context}"
119            );
120        }
121
122        if *val > one_plus_eps * *target {
123            panic!(
124                "assert_within failed at {file}:{line}:\n`{val_str}` was greater than (1 ± {eps}) * `{target_str}`\nleft:  {val}\nright: {target}\n{context}"
125            );
126        }
127    } else {
128        if *val < one_plus_eps * *target {
129            panic!(
130                "assert_within failed at {file}:{line}:\n`{val_str}` was less than (1 ± {eps}) * `{target_str}`\nleft:  {val}\nright: {target}\n{context}"
131            );
132        }
133        if *val > one_minus_eps * *target {
134            panic!(
135                "assert_within failed at {file}:{line}:\n`{val_str}` was greater than (1 ± {eps}) * `{target_str}`\nleft:  {val}\nright: {target}\n{context}"
136            );
137        }
138    }
139}
140
141#[macro_export]
142macro_rules! assert_within {
143    (+ $epsilon:expr, $val:expr, $target:expr) => {
144        $crate::assert_within_add_impl(
145            file!(),
146            line!(),
147            $val,
148            stringify!($val),
149            $target,
150            stringify!($target),
151            $epsilon,
152            format_args!(""),
153        )
154    };
155
156    (+ $epsilon:expr, $val:expr, $target:expr, $($fmt_args:tt)*) => {
157        $crate::assert_within_add_impl(
158            file!(),
159            line!(),
160            $val,
161            stringify!($val),
162            $target,
163            stringify!($target),
164            $epsilon,
165            format_args!($($fmt_args)*),
166        )
167    };
168
169    (~ $epsilon:expr, $val:expr, $target:expr) => {
170        $crate::assert_within_mul_impl(
171            file!(),
172            line!(),
173            $val,
174            stringify!($val),
175            $target,
176            stringify!($target),
177            $epsilon,
178            format_args!(""),
179        )
180    };
181
182    (~ $epsilon:expr, $val:expr, $target:expr, $($fmt_args:tt)*) => {
183        $crate::assert_within_mul_impl(
184            file!(),
185            line!(),
186            $val,
187            stringify!($val),
188            $target,
189            stringify!($target),
190            $epsilon,
191            format_args!($($fmt_args)*),
192        )
193    };
194}