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 ±∞.
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 ±∞.
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}