engineering_repr/
lib.rs

1// (c) 2024 Ross Younger
2
3#![doc = include_str!("../README.md")]
4//!
5//! # Feature flags
6#![cfg_attr(
7    feature = "document-features",
8    cfg_attr(doc, doc = ::document_features::document_features!())
9)]
10
11use std::cmp::Ordering;
12use std::num::Saturating;
13
14use num_traits::{checked_pow, ConstOne, ConstZero, PrimInt, ToPrimitive};
15
16mod string;
17pub use string::{DisplayAdapter, EngineeringRepr};
18
19mod float;
20
21#[cfg(feature = "serde")]
22mod serde_support;
23
24/// A helper type for expressing numbers in engineering notation.
25///
26/// These numbers may be converted to and from integers, strings, and [`num_rational::Ratio`]. They may also be
27/// converted to floats.
28///
29/// # Type parameter
30/// The type parameter `T` is the underlying storage type used for the significand of the number.
31/// That is to say, an `EngineeringQuantity<u32>` uses a `u32` to store the numeric part.
32#[derive(Debug, Clone, Copy, Default)]
33pub struct EngineeringQuantity<T: EQSupported<T>> {
34    /// Significant bits
35    significand: T,
36    /// Engineering exponent i.e. powers of 1e3
37    exponent: i8,
38}
39
40/////////////////////////////////////////////////////////////////////////
41// META (SUPPORTED STORAGE TYPES)
42
43/// Marker trait indicating that a type is supported as a storage type for [`EngineeringQuantity`].
44pub trait EQSupported<T: PrimInt>:
45    PrimInt
46    + std::fmt::Display
47    + ConstZero
48    + ConstOne
49    + SignHelper<T>
50    + TryInto<i64>
51    + TryInto<i128>
52    + TryInto<u64>
53    + TryInto<u128>
54{
55    /// Always 1000 (used internally)
56    const EXPONENT_BASE: T;
57}
58
59macro_rules! supported_types {
60    {$($t:ty),+} => {$(
61        impl<> EQSupported<$t> for $t {
62            const EXPONENT_BASE: $t = 1000;
63        }
64    )+}
65}
66
67supported_types!(i16, i32, i64, i128, isize, u16, u32, u64, u128, usize);
68
69/// Signedness helper data, used by string conversions
70#[derive(Debug, Clone)]
71pub struct AbsAndSign<T: PrimInt> {
72    abs: T,
73    negative: bool,
74}
75
76/// Signedness helper trait, used by string conversions.
77///
78/// This trait exists because `abs` is, quite reasonably, only implemented
79/// for types which impl [`num_traits::Signed`].
80pub trait SignHelper<T: PrimInt> {
81    /// Unpacks a maybe-signed integer into its absolute value and sign bit
82    fn abs_and_sign(&self) -> AbsAndSign<T>;
83}
84
85macro_rules! impl_unsigned_helpers {
86    {$($t:ty),+} => {$(
87        impl<> SignHelper<$t> for $t {
88            fn abs_and_sign(&self) -> AbsAndSign<$t> {
89                AbsAndSign { abs: *self, negative: false }
90            }
91        }
92    )+}
93}
94
95macro_rules! impl_signed_helpers {
96    {$($t:ty),+} => {$(
97        impl<> SignHelper<$t> for $t {
98            fn abs_and_sign(&self) -> AbsAndSign<$t> {
99                AbsAndSign { abs: self.abs(), negative: self.is_negative() }
100            }
101        }
102    )+}
103}
104
105impl_unsigned_helpers!(u16, u32, u64, u128, usize);
106impl_signed_helpers!(i16, i32, i64, i128, isize);
107
108/////////////////////////////////////////////////////////////////////////
109// BASICS
110
111// Constructors & accessors
112impl<T: EQSupported<T>> EngineeringQuantity<T> {
113    /// Raw constructor from component parts
114    ///
115    /// Construction fails if the number would overflow the storage type `T`.
116    pub fn from_raw(significand: T, exponent: i8) -> Result<Self, Error> {
117        Self::from_raw_unchecked(significand, exponent).check_for_int_overflow()
118    }
119    /// Raw accessor to retrieve the component parts
120    #[must_use]
121    pub fn to_raw(self) -> (T, i8) {
122        (self.significand, self.exponent)
123    }
124    /// Internal raw constructor
125    fn from_raw_unchecked(significand: T, exponent: i8) -> Self {
126        Self {
127            significand,
128            exponent,
129        }
130    }
131}
132
133// Comparisons
134
135impl<T: EQSupported<T> + From<EngineeringQuantity<T>>> PartialEq for EngineeringQuantity<T> {
136    /// ```
137    /// use engineering_repr::EngineeringQuantity as EQ;
138    /// let q1 = EQ::from_raw(42u32,0);
139    /// let q2 = EQ::from_raw(42u32,0);
140    /// assert_eq!(q1, q2);
141    /// let q3 = EQ::from_raw(42,1);
142    /// let q4 = EQ::from_raw(42000,0);
143    /// assert_eq!(q3, q4);
144    /// ```
145    fn eq(&self, other: &Self) -> bool {
146        // Easy case first
147        if self.exponent == other.exponent {
148            return self.significand == other.significand;
149        }
150        let cmp = self.partial_cmp(other);
151        matches!(cmp, Some(Ordering::Equal))
152    }
153}
154
155impl<T: EQSupported<T> + From<EngineeringQuantity<T>>> Eq for EngineeringQuantity<T> {}
156
157impl<T: EQSupported<T> + From<EngineeringQuantity<T>>> PartialOrd for EngineeringQuantity<T> {
158    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
159        Some(self.cmp(other))
160    }
161}
162
163impl<T: EQSupported<T> + From<EngineeringQuantity<T>>> Ord for EngineeringQuantity<T> {
164    /// ```
165    /// use engineering_repr::EngineeringQuantity as EQ;
166    /// use assertables::assert_lt;
167    /// let q2 = EQ::from_raw(41999,0).unwrap();
168    /// let q3 = EQ::from_raw(42,1).unwrap();
169    /// let q4 = EQ::from_raw(42001,0).unwrap();
170    /// assert_lt!(q2, q3);
171    /// assert_lt!(q3, q4);
172    /// ```
173    fn cmp(&self, other: &Self) -> Ordering {
174        if self.exponent == other.exponent {
175            return self.significand.cmp(&other.significand);
176        }
177        // Scale one to meet the other
178        let diff = self.exponent - other.exponent;
179        let diff_abs: u32 = diff.unsigned_abs().into();
180        if diff < 0 {
181            let scaled = other.significand * T::EXPONENT_BASE.pow(diff_abs);
182            self.significand.cmp(&scaled)
183        } else {
184            let scaled_self = self.significand * T::EXPONENT_BASE.pow(diff_abs);
185            scaled_self.cmp(&other.significand)
186        }
187    }
188}
189
190// Storage type conversion
191impl<T: EQSupported<T>> EngineeringQuantity<T> {
192    /// Conversion to a different storage type.
193    /// If you can convert from type A to type B,
194    /// then you can convert from `EngineeringQuantity<A>` to `EngineeringQuantity<B>`.
195    /// ```
196    /// use engineering_repr::EngineeringQuantity as EQ;
197    /// let q = EQ::from_raw(42u32, 0).unwrap();
198    /// let q2 = q.convert::<u64>();
199    /// assert_eq!(q2.to_raw(), (42u64, 0));
200    /// ```
201    pub fn convert<U: EQSupported<U> + From<T>>(&self) -> EngineeringQuantity<U> {
202        let (sig, exp) = self.to_raw();
203        EngineeringQuantity::<U>::from_raw_unchecked(sig.into(), exp)
204    }
205
206    /// Fallible conversion to a different storage type.
207    ///
208    /// Conversion fails if the number cannot be represented in the the destination storage type.
209    /// ```
210    /// type EQ = engineering_repr::EngineeringQuantity<u32>;
211    /// let million = EQ::from_raw(1, 2).unwrap();
212    /// let r1 = million.try_convert::<u32>().unwrap();
213    /// let r2 = million.try_convert::<u16>().expect_err("overflow"); // Overflow, because 1_000_000 won't fit into a u16
214    /// ```
215    pub fn try_convert<U: EQSupported<U> + TryFrom<T>>(
216        &self,
217    ) -> Result<EngineeringQuantity<U>, Error> {
218        let (sig, exp) = self.to_raw();
219        EngineeringQuantity::<U>::from_raw(sig.try_into().map_err(|_| Error::Overflow)?, exp)
220    }
221
222    /// Scales the number to remove any unnecessary groups of trailing zeroes.
223    #[must_use]
224    pub fn normalise(self) -> Self {
225        let mut working = self;
226        loop {
227            let (div, rem) = (
228                working.significand / T::EXPONENT_BASE,
229                working.significand % T::EXPONENT_BASE,
230            );
231            if rem != T::ZERO {
232                break;
233            }
234            working.significand = div;
235            working.exponent += 1;
236        }
237        working
238    }
239}
240
241/////////////////////////////////////////////////////////////////////////
242// CONVERSION FROM INTEGER
243
244impl<T: EQSupported<T>, U: EQSupported<U>> From<T> for EngineeringQuantity<U>
245where
246    U: From<T>,
247{
248    /// Integers can always be promoted on conversion to [`EngineeringQuantity`].
249    /// (For demotions, you have to convert the primitive yourself and handle any failures.)
250    /// ```
251    /// let i = 42u32;
252    /// let _e = engineering_repr::EngineeringQuantity::<u64>::from(i);
253    /// ```
254    fn from(value: T) -> Self {
255        Self {
256            significand: value.into(),
257            exponent: 0,
258        }
259    }
260}
261
262/////////////////////////////////////////////////////////////////////////
263// CONVERSION TO INTEGER
264
265impl<T: EQSupported<T>> EngineeringQuantity<T> {
266    fn check_for_int_overflow(self) -> Result<Self, Error> {
267        let exp: usize = self.exponent.unsigned_abs().into();
268        let Some(factor) = checked_pow(T::EXPONENT_BASE, exp) else {
269            return Err(if self.exponent < 0 {
270                Error::Underflow
271            } else {
272                Error::Overflow
273            });
274        };
275        let result: T = factor
276            .checked_mul(&self.significand)
277            .ok_or(Error::Overflow)?;
278        let _ = std::convert::TryInto::<T>::try_into(result).map_err(|_| Error::Overflow)?;
279        Ok(self)
280    }
281}
282
283macro_rules! impl_from {
284    {$($t:ty),+} => {$(
285        impl<T: EQSupported<T>> From<EngineeringQuantity<T>> for $t
286        where $t: From<T>,
287        {
288            /// Conversion to the same storage type (or a larger type)
289            /// is infallible due to the checks at construction time.
290            ///
291            /// <div class="danger">
292            /// This is a lossy conversion, any fractional part will be truncated.
293            /// </div>
294            ///
295            /// Note that if you have [`num_traits`] in scope, you may need to rephrase the conversion as `TryInto::<T>::try_into()`.
296            fn from(eq: EngineeringQuantity<T>) -> Self {
297                let abs_exp: u32 = eq.exponent.unsigned_abs().into();
298                let factor: Saturating<Self> = Saturating(T::EXPONENT_BASE.into());
299                let factor = factor.pow(abs_exp);
300                if eq.exponent > 0 {
301                    Self::from(eq.significand) * factor.0
302                } else {
303                    Self::from(eq.significand) / factor.0
304                }
305            }
306        }
307
308    )+}
309}
310
311impl_from!(u16, u32, u64, u128, usize, i16, i32, i64, i128, isize);
312
313impl<T: EQSupported<T>> EngineeringQuantity<T> {
314    fn apply_factor<U: EQSupported<U>>(self, sig: U) -> Option<U> {
315        let abs_exp: usize = self.exponent.unsigned_abs().into();
316        let factor = checked_pow(U::EXPONENT_BASE, abs_exp)?;
317        Some(if self.exponent >= 0 {
318            sig * factor
319        } else {
320            sig / factor
321        })
322    }
323}
324
325impl<T: EQSupported<T>> ToPrimitive for EngineeringQuantity<T>
326where
327    f64: TryFrom<EngineeringQuantity<T>>,
328{
329    /// Converts `self` to an `i64`. If the value cannot be represented by an `i64`, then `None` is returned.
330    /// ```
331    /// use num_traits::cast::ToPrimitive as _;
332    /// let e = engineering_repr::EngineeringQuantity::<u32>::from(65_537u32);
333    /// assert_eq!(e.to_u128(), Some(65_537));
334    /// assert_eq!(e.to_u64(), Some(65_537));
335    /// assert_eq!(e.to_u16(), None); // overflow
336    /// assert_eq!(e.to_i128(), Some(65_537));
337    /// assert_eq!(e.to_i64(), Some(65_537));
338    /// assert_eq!(e.to_i16(), None); // overflow
339    /// ```
340    fn to_i64(&self) -> Option<i64> {
341        let i: i64 = match self.significand.try_into() {
342            Ok(ii) => ii,
343            Err(_) => return None,
344        };
345        self.apply_factor(i)
346    }
347
348    fn to_u64(&self) -> Option<u64> {
349        let i: u64 = match self.significand.try_into() {
350            Ok(ii) => ii,
351            Err(_) => return None,
352        };
353        self.apply_factor(i)
354    }
355
356    /// Converts `self` to an `i128`. If the value cannot be represented by an `i128`, then `None` is returned.
357    fn to_i128(&self) -> Option<i128> {
358        let i: i128 = match self.significand.try_into() {
359            Ok(ii) => ii,
360            Err(_) => return None,
361        };
362        self.apply_factor(i)
363    }
364
365    /// Converts `self` to a `u128`. If the value cannot be represented by a `u128`, then `None` is returned.
366    fn to_u128(&self) -> Option<u128> {
367        let i: u128 = match self.significand.try_into() {
368            Ok(ii) => ii,
369            Err(_) => return None,
370        };
371        self.apply_factor(i)
372    }
373
374    /// Converts `self` to an `f64`. If the value cannot be represented by an `f64`, then `None` is returned.
375    ///
376    /// As ever, if you need to compare floating point numbers, beware of epsilon issues.
377    /// If a precise comparison is needed then converting to a [`num_rational::Ratio`] may suit.
378    /// ```
379    /// use engineering_repr::EngineeringQuantity as EQ;
380    /// use std::str::FromStr as _;
381    /// let eq = EQ::<u32>::from_str("123m").unwrap();
382    ///
383    /// // TryFrom conversion
384    /// assert_eq!(f64::try_from(eq), Ok(0.123));
385    ///
386    /// // Conversion via ToPrimitive
387    /// use num_traits::cast::ToPrimitive as _;
388    /// assert_eq!(eq.to_f32(), Some(0.123));
389    /// assert_eq!(eq.to_f64(), Some(0.123));
390    /// ```
391    fn to_f64(&self) -> Option<f64> {
392        f64::try_from(*self).ok()
393    }
394}
395
396/////////////////////////////////////////////////////////////////////////
397// ERRORS
398
399/// Local error type returned by failing conversions
400#[derive(Clone, Copy, Debug, PartialEq, thiserror::Error)]
401#[allow(missing_docs)]
402pub enum Error {
403    #[error("Numeric overflow")]
404    Overflow,
405    #[error("Numeric underflow")]
406    Underflow,
407    #[error("The string could not be parsed")]
408    ParseError,
409    #[error("The conversion could not be completed precisely")]
410    ImpreciseConversion,
411}
412
413/////////////////////////////////////////////////////////////////////////
414
415#[cfg(test)]
416mod test {
417    use assertables::{assert_gt, assert_lt};
418
419    use super::EngineeringQuantity as EQ;
420    use super::Error as EQErr;
421
422    #[test]
423    fn integers() {
424        for i in &[1i64, -1, 100, -100, 1000, 4000, -4000, 4_000_000] {
425            let ee = EQ::from_raw(*i, 0).unwrap();
426            assert_eq!(i64::from(ee), *i);
427            let ee2 = EQ::from_raw(*i, 1).unwrap();
428            assert_eq!(i64::from(ee2), *i * 1000, "input is {}", *i);
429        }
430    }
431
432    #[test]
433    fn equality() {
434        for (a, b, c, d) in &[
435            (1i64, 0, 1i64, 0),
436            (1, 1, 1000, 0),
437            (2000, 0, 2, 1),
438            (123_000_000, 0, 123_000, 1),
439            (123_000_000, 0, 123, 2),
440            (456_000_000_000_000, 0, 456_000, 3),
441            (456_000_000_000_000, 0, 456, 4),
442        ] {
443            let e1 = EQ::from_raw(*a, *b).unwrap();
444            let e2 = EQ::from_raw(*c, *d).unwrap();
445            assert_eq!(e1, e2);
446        }
447    }
448    #[test]
449    fn comparison() {
450        for (a, b, c, d) in &[
451            (1, 0i8, 2, 0i8),
452            (1, 1, 2, 1),
453            (1001, -1, 1002, -1),
454            (4, -1, 4, -2),
455            (400, -1, 400, -2),
456        ] {
457            let e1 = EQ::from_raw(*a, *b).unwrap();
458            let e2 = EQ::from_raw(*c, *d).unwrap();
459            assert_ne!(e1, e2);
460        }
461        let a1 = EQ::from_raw(1, 2).unwrap();
462        let a2 = EQ::from_raw(2, 2).unwrap();
463        assert_gt!(a2, a1);
464        assert_lt!(a1, a2);
465    }
466
467    #[test]
468    fn conversion() {
469        let t = EQ::<u32>::from_raw(12345, 0).unwrap();
470        let u = t.convert::<u64>();
471        assert_eq!(u.to_raw().0, <u32 as Into<u64>>::into(t.to_raw().0));
472        assert_eq!(t.to_raw().1, u.to_raw().1);
473    }
474
475    #[test]
476    fn to_primitive_underflow() {
477        let _ = EQ::from_raw(1i64, -10).expect_err("underflow");
478    }
479
480    #[test]
481    fn overflow() {
482        // When the number is too big to fit into the destination type, the conversion fails.
483        let t = EQ::<u32>::from_raw(100_000, 0).unwrap();
484        let _ = t.try_convert::<u16>().expect_err("TryFromIntError");
485
486        // 10^15 is too big for a u32, so overflow:
487        assert_eq!(EQ::<u32>::from_raw(1, 5), Err(EQErr::Overflow));
488
489        // The significand and exponent may both fit on their own, but overflow when combined:
490        assert_eq!(EQ::<u64>::from_raw(100_000, 5), Err(EQErr::Overflow));
491    }
492
493    #[test]
494    fn normalise() {
495        let q = EQ::from_raw(1_000_000, 0).unwrap();
496        let q2 = q.normalise();
497        assert_eq!(q, q2);
498        assert_eq!(q2.to_raw(), (1, 2));
499    }
500
501    #[test]
502    fn to_primitive() {
503        use num_traits::ToPrimitive as _;
504        let e = EQ::<i128>::from_raw(1234, 0).unwrap();
505        assert_eq!(e.to_i8(), None);
506        assert_eq!(e.to_i16(), Some(1234));
507        assert_eq!(e.to_i32(), Some(1234));
508        assert_eq!(e.to_i64(), Some(1234));
509        assert_eq!(e.to_i128(), Some(1234));
510        assert_eq!(e.to_isize(), Some(1234));
511        assert_eq!(e.to_u8(), None);
512        assert_eq!(e.to_u16(), Some(1234));
513        assert_eq!(e.to_u32(), Some(1234));
514        assert_eq!(e.to_u64(), Some(1234));
515        assert_eq!(e.to_u128(), Some(1234));
516        assert_eq!(e.to_usize(), Some(1234));
517
518        // negatives cannot fit into an unsigned
519        let e = EQ::<i128>::from_raw(-1, 0).unwrap();
520        assert_eq!(e.to_u64(), None);
521        assert_eq!(e.to_u128(), None);
522
523        // positives which would overflow
524        let e = EQ::<u128>::from_raw(u128::MAX, 0).unwrap();
525        assert_eq!(e.to_i64(), None);
526        assert_eq!(e.to_i128(), None);
527
528        // rounding toward zero
529        let e = EQ::from_raw(1, -1).unwrap();
530        assert_eq!(e.to_i32(), Some(0));
531        let e = EQ::from_raw(1001, -1).unwrap();
532        assert_eq!(e.to_i32(), Some(1));
533        let e = EQ::from_raw(-1, -1).unwrap();
534        assert_eq!(e.to_i32(), Some(0));
535        let e = EQ::from_raw(-1001, -1).unwrap();
536        assert_eq!(e.to_i32(), Some(-1));
537    }
538}