l_system_fractals/
num_validity.rs

1// src/num_validity.rs
2//
3// This file is part of l-system-fractals
4//
5// Copyright 2024 Christopher Phan
6//
7// See README.md in repository root directory for copyright/license info
8//
9// SPDX-License-Identifier: MIT OR Apache-2.0
10
11//! Functions to screen out NaN and ±∞, as well as test for near equality.
12use std::collections::HashMap;
13use std::hash;
14use std::iter::zip;
15use std::num::FpCategory;
16
17use crate::errors::LSystemError;
18
19/// A type of which two objects can be "almost equal" (i.e., have a distance less than ε).
20///
21/// # Examples
22///
23/// ```
24/// use std::collections::HashMap;
25///
26/// use l_system_fractals::num_validity::AlmostEq;
27///
28/// // Defined as (self - other).abs() < epsilon for f64 numbers
29///
30/// let a: f64 = 25.0;
31/// let b: f64 = 24.99999;
32/// assert!(a.almost_eq(&b, 0.0001));
33/// assert!(!a.almost_eq(&b, 0.00000001));
34///
35/// assert_ne!((1.57_f64).sin(), 1.0); // off by about 3.17e-07
36///
37/// assert!((1.57_f64).sin().almost_eq(&1.0, 1e-06));
38/// assert!(!(1.57_f64).sin().almost_eq(&1.0, 1e-09));
39///
40/// // Defined for HashMap<K, V> when V implements AlmostEq
41///
42/// let hm_1: HashMap<String, f64> = HashMap::from([
43///     ("alpha".into(), 10.001),
44///     ("beta".into(), -5.25001),
45/// ]);
46///
47/// let hm_2: HashMap<String, f64> = HashMap::from([
48///     ("alpha".into(), 9.999),
49///     ("beta".into(), -5.25002)
50/// ]);
51///
52/// let hm_3: HashMap<String, f64> = HashMap::from([
53///     ("alpha".into(), 10.001)
54/// ]);
55///
56/// let hm_4: HashMap<String, f64> = HashMap::from([
57///     ("alpha".into(), 10.001),
58///     ("beta".into(), -5.25001),
59///     ("gamma".into(), 3.0),
60/// ]);
61///
62/// assert!(hm_1.almost_eq(&hm_2, 0.01));
63/// assert!(!hm_1.almost_eq(&hm_2, 0.001)); // "alpha" values too far apart
64/// assert!(!hm_1.almost_eq(&hm_3, 0.01)); // hm_3 does not have key "beta"
65/// assert!(!hm_1.almost_eq(&hm_4, 0.01)); // hm_1 does not have key "gamma"
66///
67/// // Defined for Vec<T> when T implements AlmostEq
68///
69/// let v_1: Vec<f64> = vec![3.0, 2.0, -5.0];
70/// let v_2: Vec<f64> = vec![3.0, 2.0, -4.999];
71///
72/// assert!(v_1.almost_eq(&v_2, 0.002));
73/// assert!(!v_1.almost_eq(&v_2, 0.0005));
74/// ```
75///
76/// ```
77/// use std::f64::consts::PI;
78///
79/// use l_system_fractals::num_validity::AlmostEq;
80///
81/// #[derive(Clone, Copy, Debug)]
82/// struct Angle(f64);
83///
84/// impl Angle {
85///     fn reduce(&self) -> Self {
86///         Self(self.0.rem_euclid(2.0 * PI))
87///     }
88///
89///     fn abs_diff(&self, other: &Self) -> Self {
90///         let diff = (self.0 - other.0).abs().rem_euclid(2.0 * PI);
91///         Self(diff.min(2.0 * PI - diff))
92///     }
93///
94///     fn flip(&self) -> Self {
95///         Self(self.0 + PI).reduce()
96///     }
97/// }
98///
99/// impl PartialEq for Angle {
100///     fn eq(&self, other: &Self) -> bool {
101///         self.reduce() == other.reduce()
102///     }
103/// }
104///
105/// impl AlmostEq for Angle {
106///     fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
107///         self.abs_diff(&other).0 < epsilon
108///     }
109/// }
110///
111/// let a = Angle(PI/4.0);
112/// let b = Angle(9.0 * PI/4.0);
113/// let c = Angle(PI * 0.249);
114///
115/// assert!(a.almost_eq(&b, 0.00001));
116/// assert!(a.almost_eq(&c, 0.01));
117/// assert!(!a.almost_eq(&c, 0.00001));
118///
119/// #[derive(Clone, Copy, Debug)]
120/// struct PolarCoordinates {
121///     angle: Angle,
122///     radius: f64
123/// }
124///
125/// impl PolarCoordinates {
126///     fn reduce(&self) -> Self {
127///         let neg_radius: bool = self.radius < 0.0;
128///         let radius: f64 = if neg_radius { - self.radius } else { self.radius };
129///         let angle: Angle = if neg_radius { self.angle.flip() } else {self.angle.reduce()};
130///         Self { angle, radius }
131///     }
132/// }
133///
134/// impl PartialEq for PolarCoordinates {
135///     fn eq(&self, other: &Self) -> bool {
136///         self.radius == 0.0 && other.radius == 0.0 || {
137///             let s_red = self.reduce();
138///             let o_red = other.reduce();
139///             s_red.radius == o_red.radius && s_red.angle == o_red.angle
140///         }
141///     }
142/// }
143///
144/// impl AlmostEq for PolarCoordinates {
145///     fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
146///         self.radius.abs() < epsilon && other.radius.abs() < epsilon || {
147///             let s_red = self.reduce();
148///             let o_red = other.reduce();
149///             s_red.radius.almost_eq(&o_red.radius, epsilon)
150///                 && s_red.angle.almost_eq(&o_red.angle, epsilon)
151///         }
152///     }
153/// }
154///
155/// let p1 = PolarCoordinates { angle: Angle(0.25 * PI), radius: 10.0 };
156/// let p2 = PolarCoordinates { angle: Angle(2.24999 * PI), radius: 9.99999};
157///
158/// assert!(p1.almost_eq(&p2, 0.001));
159/// assert!(!p1.almost_eq(&p2, 0.000000000000001));
160///
161/// let p3 = PolarCoordinates { angle: Angle(1.25 * PI), radius: 0.0005 };
162/// let p4 = PolarCoordinates { angle: Angle(0.75 * PI), radius: 0.0005 };
163/// assert!(p3.almost_eq(&p4, 0.001));
164/// assert!(!p3.almost_eq(&p4, 0.00001));
165/// ```
166pub trait AlmostEq {
167    /// Returns `true` if the distance between objects is less than `epsilon`.
168    fn almost_eq(&self, other: &Self, epsilon: f64) -> bool;
169}
170
171impl AlmostEq for f64 {
172    fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
173        (self - other).abs() < epsilon
174    }
175}
176
177impl<T> AlmostEq for Vec<T>
178where
179    T: AlmostEq,
180{
181    fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
182        (self.len() == other.len())
183            && zip(self.iter(), other.iter()).all(|(p, q)| p.almost_eq(q, epsilon))
184    }
185}
186
187impl<K, V> AlmostEq for HashMap<K, V>
188where
189    V: AlmostEq,
190    K: Eq + PartialEq + hash::Hash,
191{
192    fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
193        for (key, val) in self.iter() {
194            match other.get(key) {
195                None => {
196                    return false;
197                }
198                Some(val2) => {
199                    if !val.almost_eq(val2, epsilon) {
200                        return false;
201                    }
202                }
203            }
204        }
205        other.keys().all(|k| self.contains_key(k))
206    }
207}
208
209/// Returns `true` if value is neither NaN nor &pm;&infin;.
210///
211/// # Examples
212///
213/// ```
214/// use std::num::FpCategory;
215///
216/// use l_system_fractals::num_validity::is_valid;
217///
218/// let n: f64 = 0.2024_f64.sin();
219/// assert_eq!(n.classify(), FpCategory::Normal);
220/// assert!(is_valid(n));
221///
222/// let z: f64 = 0.0;
223/// assert_eq!(z.classify(), FpCategory::Zero);
224/// assert!(is_valid(z));
225///
226/// let s: f64 = f64::MIN_POSITIVE / 2.0;
227/// assert_eq!(s.classify(), FpCategory::Subnormal);
228/// assert!(is_valid(s));
229///
230/// let i1: f64 = f64::INFINITY;
231/// assert_eq!(i1.classify(), FpCategory::Infinite);
232/// assert!(!is_valid(i1));
233///
234/// let i2: f64 = f64::NEG_INFINITY;
235/// assert_eq!(i2.classify(), FpCategory::Infinite);
236/// assert!(!is_valid(i2));
237///
238/// let na: f64 = f64::NAN;
239/// assert_eq!(na.classify(), FpCategory::Nan);
240/// assert!(!is_valid(na));
241/// ```
242pub fn is_valid(x: f64) -> bool {
243    let cat: FpCategory = x.classify();
244    (cat == FpCategory::Zero) || (cat == FpCategory::Subnormal) || (cat == FpCategory::Normal)
245}
246
247/// Raises an error if the value is either NaN or &pm;&infin;.
248///
249/// The error raised is [`LSystemError::InvalidFloat`].
250///
251/// # Examples
252///
253/// ```
254/// use std::num::FpCategory;
255///
256/// use l_system_fractals::num_validity::err_if_invalid;
257///
258/// let n: f64 = 0.2024_f64.sin();
259/// assert_eq!(n.classify(), FpCategory::Normal);
260/// assert!(err_if_invalid(n).is_ok());
261///
262/// let z: f64 = 0.0;
263/// assert_eq!(z.classify(), FpCategory::Zero);
264/// assert!(err_if_invalid(z).is_ok());
265///
266/// let s: f64 = f64::MIN_POSITIVE / 2.0;
267/// assert_eq!(s.classify(), FpCategory::Subnormal);
268/// assert!(err_if_invalid(s).is_ok());
269///
270/// let i1: f64 = f64::INFINITY;
271/// assert_eq!(i1.classify(), FpCategory::Infinite);
272/// assert!(err_if_invalid(i1).is_err());
273///
274/// let i2: f64 = f64::NEG_INFINITY;
275/// assert_eq!(i2.classify(), FpCategory::Infinite);
276/// assert!(err_if_invalid(i2).is_err());
277///
278/// let na: f64 = f64::NAN;
279/// assert_eq!(na.classify(), FpCategory::Nan);
280/// assert!(err_if_invalid(na).is_err());
281/// ```
282pub fn err_if_invalid(x: f64) -> Result<f64, LSystemError> {
283    if is_valid(x) {
284        Ok(x)
285    } else {
286        Err(LSystemError::InvalidFloat)
287    }
288}
289
290/// Returns the maximum value of the slice ignoring any values that are
291/// [NaN or infinite](is_valid()).
292///
293/// # Examples
294///
295/// ```
296/// use l_system_fractals::num_validity::max;
297///
298/// assert_eq!(max(&[0.25, 0.0, f64::MIN_POSITIVE]).unwrap(), 0.25);
299///
300/// // f64::INFINITY ignored
301/// assert_eq!(max(&[f64::INFINITY, 0.25, 0.0, f64::MIN_POSITIVE]).unwrap(), 0.25);
302///
303/// // no valid values
304/// assert!(max(&[f64::INFINITY, f64::NAN]).is_none());
305/// assert!(max(&[]).is_none());
306/// ```
307pub fn max(vals: &[f64]) -> Option<f64> {
308    let mut current_max: Option<f64> = None;
309    for k in vals {
310        if is_valid(*k) {
311            if current_max.is_none() {
312                current_max = Some(*k);
313            } else {
314                current_max = Some(k.max(current_max.unwrap()));
315            }
316        }
317    }
318    current_max
319}
320
321/// Returns the minimum value of the slice ignoring any values that are
322/// [NaN or infinite](is_valid()).
323///
324/// # Examples
325///
326/// ```
327/// use l_system_fractals::num_validity::min;
328///
329/// assert_eq!(min(&[0.25, f64::MIN_POSITIVE, 100.0]).unwrap(), f64::MIN_POSITIVE);
330///
331/// // f64::NEG_INFINITY ignored
332/// assert_eq!(
333///     min(&[0.25, f64::MIN_POSITIVE, 100.0, f64::NEG_INFINITY]).unwrap(),
334///     f64::MIN_POSITIVE
335/// );
336///
337/// // no valid values
338/// assert!(min(&[f64::INFINITY, f64::NAN]).is_none());
339/// assert!(min(&[]).is_none());
340/// ```
341pub fn min(vals: &[f64]) -> Option<f64> {
342    let mut current_min: Option<f64> = None;
343    for k in vals {
344        if is_valid(*k) {
345            if current_min.is_none() {
346                current_min = Some(*k);
347            } else {
348                current_min = Some(k.min(current_min.unwrap()));
349            }
350        }
351    }
352    current_min
353}