1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[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 #[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
30pub 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#[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 #[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 #[must_use]
68 pub fn is_compatible_with(self, other: Self) -> bool {
69 self.domain == other.domain && self.quantity == other.quantity
70 }
71
72 #[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
83pub 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}