unitscale_core 0.1.7

UnitScale core and traits for simplifying conversions over bus communication
Documentation
// Copyright 2025 GooseGrid Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#![doc = include_str!("../README.md")]

/// Errors relating to data conversion
#[derive(thiserror::Error, Debug, PartialEq)]
pub enum UnitScaleError {
    #[error("Outside of type bounds: {0}")]
    Conversion(String),
    #[error("Unknown error")]
    Unknown(String),
}

/// Trait for setting a scale of a custom data type for data transfers
pub trait UnitScale {
    const SCALE: f64;
}

/// Trait for custom data type for data transfers over various bus interfaces
pub trait Scaled<U> {
    fn scaled_value(&self) -> U;
}

pub trait ScaledPrimitiveByteSize {
    fn primitive_byte_size() -> usize;
}

#[cfg(test)]
mod tests {
    use super::*;
    use core::marker::PhantomData;
    use core::mem::discriminant;
    use float_cmp::approx_eq;
    use num_traits::{FromPrimitive, ToPrimitive};

    struct Scale0_01;

    impl UnitScale for Scale0_01 {
        const SCALE: f64 = 0.01;
    }

    struct Volts<S, U> {
        value: U,
        _scale: std::marker::PhantomData<S>,
    }

    impl<S, U> TryFrom<f64> for Volts<S, U>
    where
        S: UnitScale,
        U: FromPrimitive,
    {
        type Error = UnitScaleError;
        fn try_from(value: f64) -> Result<Self, Self::Error> {
            let scaled_value = value / S::SCALE;

            if let Some(value) = U::from_f64(scaled_value) {
                Ok(Self {
                    value,
                    _scale: PhantomData,
                })
            } else {
                Err(UnitScaleError::Conversion(format!(
                    "Scaled {} is outside of {} bounds",
                    scaled_value,
                    std::any::type_name::<U>()
                )))
            }
        }
    }

    impl<S, U> Scaled<U> for Volts<S, U>
    where
        S: UnitScale,
        U: Copy,
    {
        fn scaled_value(&self) -> U {
            self.value
        }
    }

    impl<S, U> ScaledPrimitiveByteSize for Volts<S, U>
    where
        S: UnitScale,
        U: Copy + ToPrimitive,
    {
        fn primitive_byte_size() -> usize {
            core::mem::size_of::<U>()
        }
    }

    impl<S, U> Volts<S, U>
    where
        S: UnitScale,
        U: Copy + ToPrimitive,
    {
        fn to_f64(&self) -> Option<f64> {
            self.value.to_f64().map(|v| v * S::SCALE)
        }
    }

    #[test]
    fn data_scaled_to_0_01() {
        assert_eq!(Scale0_01::SCALE, 0.01);
    }

    #[test]
    fn test_scale_0_01_for_u8() {
        let value: f64 = 1.28;
        let volts = Volts::<Scale0_01, u8>::try_from(value).unwrap();

        assert_eq!(volts.scaled_value(), 128);
        assert!(approx_eq!(
            f64,
            volts.to_f64().expect("Unable to convert to f64"),
            value,
            epsilon = 0.01
        ));
    }

    #[test]
    fn test_scale_0_01_for_u8_out_of_bounds() {
        let value: f64 = 3.01;
        let volts = Volts::<Scale0_01, u8>::try_from(value);

        assert!(volts.is_err());

        if let Err(e) = volts {
            assert_eq!(
                discriminant(&e),
                discriminant(&UnitScaleError::Conversion("".into()))
            );
        }
    }

    #[test]
    fn primitive_byte_size() {
        let byte_size = Volts::<Scale0_01, u8>::primitive_byte_size();

        assert_eq!(byte_size, 1, "For `u8` the primitive byte size should be 1");
    }
}