unitscale_core/
lib.rs

1// Copyright 2025 GooseGrid Technologies
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![doc = include_str!("../README.md")]
16
17/// Errors relating to data conversion
18#[derive(thiserror::Error, Debug, PartialEq)]
19pub enum UnitScaleError {
20    #[error("Outside of type bounds: {0}")]
21    Conversion(String),
22    #[error("Unknown error")]
23    Unknown(String),
24}
25
26/// Trait for setting a scale of a custom data type for data transfers
27pub trait UnitScale {
28    const SCALE: f64;
29}
30
31/// Trait for custom data type for data transfers over various bus interfaces
32pub trait Scaled<U> {
33    fn scaled_value(&self) -> U;
34}
35
36pub trait ScaledPrimitiveByteSize {
37    fn primitive_byte_size() -> usize;
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43    use core::marker::PhantomData;
44    use core::mem::discriminant;
45    use float_cmp::approx_eq;
46    use num_traits::{FromPrimitive, ToPrimitive};
47
48    struct Scale0_01;
49
50    impl UnitScale for Scale0_01 {
51        const SCALE: f64 = 0.01;
52    }
53
54    struct Volts<S, U> {
55        value: U,
56        _scale: std::marker::PhantomData<S>,
57    }
58
59    impl<S, U> TryFrom<f64> for Volts<S, U>
60    where
61        S: UnitScale,
62        U: FromPrimitive,
63    {
64        type Error = UnitScaleError;
65        fn try_from(value: f64) -> Result<Self, Self::Error> {
66            let scaled_value = value / S::SCALE;
67
68            if let Some(value) = U::from_f64(scaled_value) {
69                Ok(Self {
70                    value,
71                    _scale: PhantomData,
72                })
73            } else {
74                Err(UnitScaleError::Conversion(format!(
75                    "Scaled {} is outside of {} bounds",
76                    scaled_value,
77                    std::any::type_name::<U>()
78                )))
79            }
80        }
81    }
82
83    impl<S, U> Scaled<U> for Volts<S, U>
84    where
85        S: UnitScale,
86        U: Copy,
87    {
88        fn scaled_value(&self) -> U {
89            self.value
90        }
91    }
92
93    impl<S, U> ScaledPrimitiveByteSize for Volts<S, U>
94    where
95        S: UnitScale,
96        U: Copy + ToPrimitive,
97    {
98        fn primitive_byte_size() -> usize {
99            core::mem::size_of::<U>()
100        }
101    }
102
103    impl<S, U> Volts<S, U>
104    where
105        S: UnitScale,
106        U: Copy + ToPrimitive,
107    {
108        fn to_f64(&self) -> Option<f64> {
109            self.value.to_f64().map(|v| v * S::SCALE)
110        }
111    }
112
113    #[test]
114    fn data_scaled_to_0_01() {
115        assert_eq!(Scale0_01::SCALE, 0.01);
116    }
117
118    #[test]
119    fn test_scale_0_01_for_u8() {
120        let value: f64 = 1.28;
121        let volts = Volts::<Scale0_01, u8>::try_from(value).unwrap();
122
123        assert_eq!(volts.scaled_value(), 128);
124        assert!(approx_eq!(
125            f64,
126            volts.to_f64().expect("Unable to convert to f64"),
127            value,
128            epsilon = 0.01
129        ));
130    }
131
132    #[test]
133    fn test_scale_0_01_for_u8_out_of_bounds() {
134        let value: f64 = 3.01;
135        let volts = Volts::<Scale0_01, u8>::try_from(value);
136
137        assert!(volts.is_err());
138
139        if let Err(e) = volts {
140            assert_eq!(
141                discriminant(&e),
142                discriminant(&UnitScaleError::Conversion("".into()))
143            );
144        }
145    }
146
147    #[test]
148    fn primitive_byte_size() {
149        let byte_size = Volts::<Scale0_01, u8>::primitive_byte_size();
150
151        assert_eq!(byte_size, 1, "For `u8` the primitive byte size should be 1");
152    }
153}