1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Clone, Copy, Debug, PartialEq)]
6pub struct Measurement {
7 pub value: f64,
8 pub unit: &'static str,
9}
10
11impl Measurement {
12 #[must_use]
14 pub const fn new(value: f64, unit: &'static str) -> Self {
15 Self { value, unit }
16 }
17
18 #[must_use]
20 pub fn convert(self, conversion: Conversion) -> Option<Self> {
21 if self.unit != conversion.from_unit {
22 return None;
23 }
24
25 Some(Self::new(conversion.apply(self.value), conversion.to_unit))
26 }
27}
28
29#[derive(Clone, Copy, Debug, PartialEq)]
31pub struct Conversion {
32 pub from_unit: &'static str,
33 pub to_unit: &'static str,
34 pub factor: f64,
35 pub offset: f64,
36}
37
38impl Conversion {
39 #[must_use]
41 pub const fn linear(from_unit: &'static str, to_unit: &'static str, factor: f64) -> Self {
42 Self::affine(from_unit, to_unit, factor, 0.0)
43 }
44
45 #[must_use]
47 pub const fn affine(
48 from_unit: &'static str,
49 to_unit: &'static str,
50 factor: f64,
51 offset: f64,
52 ) -> Self {
53 Self {
54 from_unit,
55 to_unit,
56 factor,
57 offset,
58 }
59 }
60
61 #[must_use]
63 pub fn apply(self, value: f64) -> f64 {
64 value * self.factor + self.offset
65 }
66}
67
68#[must_use]
70pub fn compose(first: Conversion, second: Conversion) -> Option<Conversion> {
71 if first.to_unit != second.from_unit {
72 return None;
73 }
74
75 Some(Conversion::affine(
76 first.from_unit,
77 second.to_unit,
78 first.factor * second.factor,
79 first.offset * second.factor + second.offset,
80 ))
81}
82
83pub mod prelude {
85 pub use super::{Conversion, Measurement, compose};
86}
87
88#[cfg(test)]
89mod tests {
90 use super::{Conversion, Measurement, compose};
91
92 #[test]
93 fn converts_linear_measurements() {
94 let distance = Measurement::new(2.0, "km");
95 let conversion = Conversion::linear("km", "m", 1_000.0);
96
97 assert_eq!(
98 distance.convert(conversion),
99 Some(Measurement::new(2_000.0, "m"))
100 );
101 }
102
103 #[test]
104 fn supports_affine_conversions() {
105 let temperature = Measurement::new(20.0, "C");
106 let conversion = Conversion::affine("C", "F", 1.8, 32.0);
107
108 assert_eq!(
109 temperature.convert(conversion),
110 Some(Measurement::new(68.0, "F"))
111 );
112 }
113
114 #[test]
115 fn composes_compatible_conversions() {
116 let kilometers_to_meters = Conversion::linear("km", "m", 1_000.0);
117 let meters_to_centimeters = Conversion::linear("m", "cm", 100.0);
118 let kilometers_to_centimeters =
119 compose(kilometers_to_meters, meters_to_centimeters).expect("units should chain");
120
121 assert_eq!(kilometers_to_centimeters.apply(1.5), 150_000.0);
122 }
123
124 #[test]
125 fn rejects_incompatible_conversion_chains() {
126 let seconds_to_minutes = Conversion::linear("s", "min", 1.0 / 60.0);
127 let meters_to_centimeters = Conversion::linear("m", "cm", 100.0);
128
129 assert_eq!(compose(seconds_to_minutes, meters_to_centimeters), None);
130 }
131}