engineering_repr/
float.rs

1//! Conversions to [`num_rational::Ratio`] and float
2
3use num_rational::Ratio;
4use num_traits::checked_pow;
5
6use crate::{EQSupported, EngineeringQuantity, Error};
7
8/////////////////////////////////////////////////////////////////////////////////
9// RATIO
10
11impl<T: EQSupported<T> + num_integer::Integer + std::convert::From<EngineeringQuantity<T>>>
12    TryFrom<EngineeringQuantity<T>> for Ratio<T>
13{
14    type Error = Error;
15
16    fn try_from(value: EngineeringQuantity<T>) -> Result<Self, Self::Error> {
17        Ok(if value.exponent >= 0 {
18            // it cannot have a fractional part
19            let result: T = value.into();
20            Ratio::new(Into::<T>::into(result), T::ONE)
21        } else {
22            let denom: T = checked_pow(T::EXPONENT_BASE, value.exponent.unsigned_abs().into())
23                .ok_or(Error::Underflow)?;
24            Ratio::new(value.significand, denom)
25        })
26    }
27}
28
29impl<T: EQSupported<T>> TryFrom<Ratio<T>> for EngineeringQuantity<T>
30where
31    T: num_integer::Integer,
32{
33    type Error = Error;
34
35    /// This is a precise conversion, which only succeeds if the denominator of the input Ratio is a power of 1000.
36    fn try_from(value: Ratio<T>) -> Result<Self, Self::Error> {
37        let (num, mut denom) = value.into_raw();
38        let (sig, exp) = if denom == T::ONE {
39            (num, 0i8)
40        } else {
41            let mut exp = 0i8;
42            // Scale away any powers of 1000
43            loop {
44                let (div, rem) = denom.div_rem(&T::EXPONENT_BASE);
45                if div == T::ZERO || rem != T::ZERO {
46                    break;
47                }
48                exp -= 1;
49                denom = div;
50            }
51
52            // if 1000 divides by denom precisely, we can scale up to make a precise conversion
53            let (scale, rem) = T::EXPONENT_BASE.div_rem(&denom);
54            if rem != T::ZERO {
55                return Err(Error::ImpreciseConversion);
56            }
57            // The denominator is _divided_ by scale, which means we're rounding up to the next exponent.
58            // Even when the denominator is 1, this logic still works, though it might overflow so special-case it.
59            if scale == T::EXPONENT_BASE {
60                (num, exp)
61            } else {
62                (num * scale, exp - 1)
63            }
64        };
65        EngineeringQuantity::from_raw(sig, exp)
66    }
67}
68
69/////////////////////////////////////////////////////////////////////////////////
70// FLOAT
71
72impl<T: EQSupported<T>> TryFrom<EngineeringQuantity<T>> for f64
73where
74    Ratio<T>: num_traits::ToPrimitive,
75    Ratio<T>: TryFrom<EngineeringQuantity<T>, Error = crate::Error>,
76{
77    type Error = Error;
78
79    fn try_from(value: EngineeringQuantity<T>) -> Result<Self, Self::Error> {
80        use num_traits::ToPrimitive as _;
81        let r = Ratio::<T>::try_from(value)?;
82        r.to_f64().ok_or(Error::ImpreciseConversion)
83    }
84}
85
86/////////////////////////////////////////////////////////////////////////////////
87
88#[cfg(test)]
89mod test {
90    use std::str::FromStr as _;
91
92    use super::EngineeringQuantity as EQ;
93    use super::Error;
94    use num_rational::Ratio;
95    use num_traits::ToPrimitive;
96
97    #[test]
98    fn to_ratio() {
99        for (sig, exp, num, denom) in &[
100            (1i64, 0i8, 1i64, 1i64),
101            (1, 1, 1000, 1),
102            (27, 2, 27_000_000, 1),
103            (1, -1, 1, 1000),
104            (4, -3, 4, 1_000_000_000),
105            (12_345, -1, 12_345, 1000),
106            (9, 6, 9_000_000_000_000_000_000, 1),
107            (-9, -6, -9, 1_000_000_000_000_000_000),
108        ] {
109            let eq = EQ::from_raw(*sig, *exp).unwrap();
110            let ratio: Ratio<i64> = eq.try_into().unwrap();
111            assert_eq!(ratio, Ratio::new(*num, *denom));
112        }
113    }
114
115    #[test]
116    fn to_ratio_errors() {
117        for (sig, exp, err) in &[
118            (1i64, -7, Error::Underflow),
119            (1_000_000i64, -7, Error::Underflow), // This quantity is technically valid but getting there underflows
120            (1i64, -11, Error::Underflow),
121        ] {
122            let eq = EQ::from_raw_unchecked(*sig, *exp);
123            let ratio = std::convert::TryInto::<Ratio<i64>>::try_into(eq);
124            assert_eq!(ratio, Err(*err), "case: {}, {}", *sig, *exp);
125        }
126    }
127
128    #[test]
129    fn from_ratio() {
130        for (num, denom, sig, exp) in &[
131            (1i64, 1i64, 1i64, 0i8),
132            (1000, 1, 1, 1),
133            (27_000_000, 1, 27, 2),
134            (1, 1000, 1, -1),
135            (4, 1_000_000_000, 4, -3),
136            (12_345, 1000, 12_345, -1),
137            (9_000_000_000_000_000_000, 1, 9, 6),
138            (-9, 1_000_000_000_000_000_000, -9, -6),
139        ] {
140            let ratio = Ratio::new(*num, *denom);
141            let eq: EQ<i64> = ratio.try_into().unwrap();
142            let expected = EQ::from_raw(*sig, *exp).unwrap();
143            assert_eq!(eq, expected, "inputs: {num:?}, {denom:?}",);
144        }
145    }
146
147    #[test]
148    fn from_ratio_errors() {
149        let ratio = Ratio::new(1, 333);
150        let result = EQ::<i64>::try_from(ratio).unwrap_err();
151        assert_eq!(result, Error::ImpreciseConversion);
152    }
153
154    const FLOAT_TEST_CASES: &[(&str, f64)] = &[
155        ("42", 42.0),
156        ("1m", 0.001),
157        ("1001m", 1.001),
158        ("1001100m", 1001.1),
159        ("1001100u", 1.001_1),
160        ("43M5", 43_500_000.0),
161        ("1a", 0.000_000_000_000_000_001),
162    ];
163
164    #[test]
165    fn to_f64() {
166        use assertables::assert_in_epsilon;
167
168        for (s, expected) in FLOAT_TEST_CASES {
169            let eq = EQ::<i64>::from_str(s).unwrap();
170            let f = eq.to_f64().unwrap();
171            assert_in_epsilon!(f, *expected, f64::EPSILON);
172        }
173
174        for s in &["1z", "1y", "1r", "1q"] {
175            let eq = EQ::<i64>::from_str(s);
176            assert_eq!(eq, Err(Error::Underflow));
177        }
178    }
179    #[test]
180    #[allow(clippy::cast_possible_truncation)]
181    fn to_f32() {
182        use assertables::assert_in_epsilon;
183
184        for (s, expected) in FLOAT_TEST_CASES {
185            let eq = EQ::<i64>::from_str(s).unwrap();
186            let f = eq.to_f32().unwrap();
187            assert_in_epsilon!(f, (*expected) as f32, f32::EPSILON);
188        }
189    }
190}