automatica 1.0.0

Automatic control systems library
Documentation
//! # Units of measurement
//!
//! List of strongly typed units of measurement. It avoids the use of primitive
//! types (newtype pattern)
//! * decibel
//! * seconds
//! * Hertz
//! * radians per second
//!
//! Conversion between units are available.

use std::{
    convert::From,
    fmt::{Display, Formatter, LowerExp, UpperExp},
    ops::{Add, Div, Mul},
};

use crate::{Const, Inv, Zero};

/// Macro to implement Display trait for units. It passes the formatter options
/// to the unit inner type.
///
/// # Examples
/// ```text
/// impl_display!(Seconds);
/// ```
macro_rules! impl_display {
    ($name:ident) => {
        /// Format the unit as its inner type.
        impl<T: Display> Display for $name<T> {
            fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
                Display::fmt(&self.0, f)
            }
        }

        /// Format the unit as its inner type.
        impl<T: LowerExp> LowerExp for $name<T> {
            fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
                LowerExp::fmt(&self.0, f)
            }
        }

        /// Format the unit as its inner type.
        impl<T: UpperExp> UpperExp for $name<T> {
            fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
                UpperExp::fmt(&self.0, f)
            }
        }
    };
}

/// Trait for the conversion to decibels.
pub trait ToDecibel {
    /// Convert to decibels
    fn to_db(&self) -> Self;
}

/// Implementation of the Decibels for f64
impl ToDecibel for f64 {
    /// Convert f64 to decibels
    fn to_db(&self) -> Self {
        20. * self.log10()
    }
}

/// Implementation of the Decibels for f32
impl ToDecibel for f32 {
    /// Convert f32 to decibels
    fn to_db(&self) -> Self {
        20. * self.log10()
    }
}

#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
/// Unit of measurement: deciBel \[dB\]
pub struct Decibel<T>(pub T);

/// Unit of measurement: seconds \[s\]
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct Seconds<T>(pub T);

impl<T> Add<Seconds<T>> for &Seconds<T>
where
    T: Clone + Add<Output = T>,
{
    type Output = Seconds<T>;
    fn add(self, rhs: Seconds<T>) -> Self::Output {
        Seconds(self.0.clone() + rhs.0)
    }
}

impl<T> Add for &Seconds<T>
where
    T: Clone + Add<Output = T>,
{
    type Output = Seconds<T>;
    fn add(self, rhs: Self) -> Self::Output {
        Seconds(self.0.clone() + rhs.0.clone())
    }
}

impl<T> Mul<&T> for &Seconds<T>
where
    T: Clone + Mul<Output = T>,
{
    type Output = Seconds<T>;
    fn mul(self, rhs: &T) -> Self::Output {
        Seconds(self.0.clone() * rhs.clone())
    }
}

impl<T> Mul<T> for &Seconds<T>
where
    T: Clone + Mul<Output = T>,
{
    type Output = Seconds<T>;
    fn mul(self, rhs: T) -> Self::Output {
        Seconds(self.0.clone() * rhs)
    }
}

impl<T> Zero for Seconds<T>
where
    T: Zero,
{
    fn zero() -> Self {
        Seconds(T::zero())
    }

    fn is_zero(&self) -> bool {
        self.0.is_zero()
    }
}

/// Unit of measurement: Hertz \[Hz\]
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct Hertz<T>(pub T);

/// Unit of measurement: Radians per seconds \[rad/s\]
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct RadiansPerSecond<T>(pub T);

impl_display!(Decibel);
impl_display!(Seconds);
impl_display!(Hertz);
impl_display!(RadiansPerSecond);

impl<T> From<Hertz<T>> for RadiansPerSecond<T>
where
    T: Const + Mul<Output = T>,
{
    /// Convert Hertz into radians per second.
    fn from(hz: Hertz<T>) -> Self {
        Self(T::tau() * hz.0)
    }
}

impl<T> From<RadiansPerSecond<T>> for Hertz<T>
where
    T: Div<Output = T> + Const,
{
    /// Convert radians per second into Hertz.
    fn from(rps: RadiansPerSecond<T>) -> Self {
        Self(rps.0 / T::tau())
    }
}

impl<T> Inv for Seconds<T>
where
    T: Inv<Output = T>,
{
    type Output = Hertz<T>;

    /// Convert seconds into Hertz.
    fn inv(self) -> Self::Output {
        Hertz(self.0.inv())
    }
}

impl<T> Inv for Hertz<T>
where
    T: Inv<Output = T>,
{
    type Output = Seconds<T>;

    /// Convert Hertz into seconds.
    fn inv(self) -> Self::Output {
        Seconds(self.0.inv())
    }
}

#[cfg(test)]
mod tests {
    use proptest::prelude::*;

    use super::*;

    #[test]
    fn decibel() {
        assert_abs_diff_eq!(40., 100_f64.to_db(), epsilon = 0.);
        assert_relative_eq!(-3.0103, 2_f64.recip().sqrt().to_db(), max_relative = 1e5);

        assert_abs_diff_eq!(0., 1_f32.to_db(), epsilon = 0.);
    }

    #[test]
    fn conversion() {
        let tau = 2. * std::f64::consts::PI;
        assert_eq!(RadiansPerSecond(tau), RadiansPerSecond::from(Hertz(1.0)));

        let hz = Hertz(2.0);
        assert_eq!(hz, Hertz::from(RadiansPerSecond::from(hz)));

        let rps = RadiansPerSecond(2.0);
        assert_eq!(rps, RadiansPerSecond::from(Hertz::from(rps)));
    }

    proptest! {
        #[test]
        fn qc_conversion_hertz(hz in (0.0..1e12)) {
            assert_relative_eq!(
                hz,
                Hertz::from(RadiansPerSecond::from(Hertz(hz))).0,
                max_relative = 1e-15
            );
        }
    }

    proptest! {
        #[test]
        fn qc_conversion_rps(rps in (0.0..1e12)) {
            assert_relative_eq!(
                rps,
                RadiansPerSecond::from(Hertz::from(RadiansPerSecond(rps))).0,
                max_relative = 1e-15
            );
        }
    }

    proptest! {
        #[test]
        fn qc_conversion_s_hz(s in (0.0..1e12_f32)) {
            // f32 precision.
            assert_relative_eq!(s, Seconds(s).inv().inv().0, max_relative = 1e-5);
        }
    }

    proptest! {
        #[test]
        fn qc_conversion_hz_s(hz in (0.0..1e12_f64)) {
            // f64 precision.
            assert_relative_eq!(hz, Hertz(hz).inv().inv().0, max_relative = 1e-14);
        }
    }

    #[test]
    fn format() {
        assert_eq!("0.33".to_owned(), format!("{:.2}", Seconds(1. / 3.)));
        assert_eq!("0.3333".to_owned(), format!("{:.*}", 4, Seconds(1. / 3.)));
        assert_eq!("4.20e1".to_owned(), format!("{:.2e}", Seconds(42.)));
        assert_eq!("4.20E2".to_owned(), format!("{:.2E}", Seconds(420.)));
    }

    #[test]
    fn zero_seconds() {
        assert!(Seconds::<f32>::zero().is_zero());
    }
}