Skip to main content

use_units/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4/// Domain tags for the current RustUse unit families.
5#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6pub enum UnitDomain {
7    Chemistry,
8    Finance,
9    Measure,
10    Physics,
11    Time,
12    Wave,
13}
14
15impl UnitDomain {
16    /// Returns the canonical lowercase domain label.
17    #[must_use]
18    pub const fn as_str(self) -> &'static str {
19        match self {
20            Self::Chemistry => "chemistry",
21            Self::Finance => "finance",
22            Self::Measure => "measure",
23            Self::Physics => "physics",
24            Self::Time => "time",
25            Self::Wave => "wave",
26        }
27    }
28}
29
30/// Stable list of domains currently covered by the workspace scaffold.
31pub const SUPPORTED_DOMAINS: [UnitDomain; 6] = [
32    UnitDomain::Chemistry,
33    UnitDomain::Physics,
34    UnitDomain::Wave,
35    UnitDomain::Time,
36    UnitDomain::Finance,
37    UnitDomain::Measure,
38];
39
40/// Small copyable unit descriptor for simple linear conversions.
41#[derive(Clone, Copy, Debug, PartialEq)]
42pub struct UnitSpec {
43    pub domain: UnitDomain,
44    pub quantity: &'static str,
45    pub symbol: &'static str,
46    pub scale_to_base: f64,
47}
48
49impl UnitSpec {
50    /// Creates a new unit descriptor.
51    #[must_use]
52    pub const fn new(
53        domain: UnitDomain,
54        quantity: &'static str,
55        symbol: &'static str,
56        scale_to_base: f64,
57    ) -> Self {
58        Self {
59            domain,
60            quantity,
61            symbol,
62            scale_to_base,
63        }
64    }
65
66    /// Returns whether two units can be converted with a linear scale factor.
67    #[must_use]
68    pub fn is_compatible_with(self, other: Self) -> bool {
69        self.domain == other.domain && self.quantity == other.quantity
70    }
71
72    /// Converts a value into a compatible target unit.
73    #[must_use]
74    pub fn convert_value(self, value: f64, target: Self) -> Option<f64> {
75        if !self.is_compatible_with(target) {
76            return None;
77        }
78
79        Some(value * self.scale_to_base / target.scale_to_base)
80    }
81}
82
83/// Commonly used unit primitives.
84pub mod prelude {
85    pub use super::{SUPPORTED_DOMAINS, UnitDomain, UnitSpec};
86}
87
88#[cfg(test)]
89mod tests {
90    use super::{SUPPORTED_DOMAINS, UnitDomain, UnitSpec};
91
92    #[test]
93    fn reports_supported_domains_in_expected_order() {
94        let labels: Vec<_> = SUPPORTED_DOMAINS
95            .iter()
96            .map(|domain| domain.as_str())
97            .collect();
98
99        assert_eq!(
100            labels,
101            vec!["chemistry", "physics", "wave", "time", "finance", "measure"]
102        );
103    }
104
105    #[test]
106    fn converts_compatible_linear_units() {
107        let meter = UnitSpec::new(UnitDomain::Measure, "length", "m", 1.0);
108        let kilometer = UnitSpec::new(UnitDomain::Measure, "length", "km", 1_000.0);
109
110        assert_eq!(kilometer.convert_value(2.5, meter), Some(2_500.0));
111        assert_eq!(meter.convert_value(750.0, kilometer), Some(0.75));
112    }
113
114    #[test]
115    fn rejects_incompatible_unit_conversions() {
116        let meter = UnitSpec::new(UnitDomain::Measure, "length", "m", 1.0);
117        let second = UnitSpec::new(UnitDomain::Time, "duration", "s", 1.0);
118
119        assert_eq!(meter.convert_value(1.0, second), None);
120        assert!(!meter.is_compatible_with(second));
121    }
122}