cas_unit_convert/
unit.rs

1use std::{error::Error, fmt::{self, Display, Formatter}, str::FromStr};
2use super::convert::Convert;
3
4/// A compound unit, created by combining multiple [`Unit`]s.
5#[derive(Clone, Debug, PartialEq, Eq)]
6#[non_exhaustive]
7pub struct CompoundUnit {
8    /// The units that make up this compound unit.
9    units: Vec<Unit>,
10}
11
12impl Display for CompoundUnit {
13    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
14        let Some((first, rest)) = self.units.split_first() else {
15            return Ok(());
16        };
17
18        // there is no `/` or `*` preceding the first unit, so we format it separately
19        write!(f, "{}", first)?;
20
21        for unit in rest {
22            if unit.power > 0 {
23                write!(f, "*{}", unit.base)?;
24            } else {
25                write!(f, "/{}", unit.base)?;
26            }
27            if unit.power.abs() > 1 {
28                write!(f, "^{}", unit.power.abs())?;
29            }
30        }
31
32        Ok(())
33    }
34}
35
36impl FromStr for CompoundUnit {
37    type Err = InvalidUnit;
38
39    fn from_str(value: &str) -> Result<Self, Self::Err> {
40        let mut units = Vec::new();
41
42        /// Look back in the given string and return the index of the first `*` or `/` character
43        /// found, along with the character itself.
44        fn look_back(slice: &str) -> Option<(usize, char)> {
45            let next_star = slice.rfind('*');
46            let next_div = slice.rfind('/');
47            match (next_star, next_div) {
48                // choose the later of the two characters
49                (Some(s), Some(d)) => Some((s.max(d), if s > d { '*' } else { '/' })),
50                (Some(s), None) => Some((s, '*')),
51                (None, Some(d)) => Some((d, '/')),
52                _ => None,
53            }
54        }
55
56        // value.split(&['*', '/']) is nice, but it doesn't tell us which character was used to
57        // split. we need to know to determine the sign of the power
58        //
59        // we could use value.split(&['*', '/']).enumerate() and use the index to index into the
60        // string, but that requires converting the &str to Vec<char>
61        //
62        // here is a manual approach
63        let mut start = value.len();
64        while let Some((next, c)) = look_back(&value[..start]) {
65            let mut unit: Unit = value[next + 1..start].parse()?;
66            unit.power *= if c == '*' { 1 } else { -1 };
67            units.push(unit);
68
69            start = next;
70        }
71
72        // unit at the start
73        units.push(value[..start].parse()?);
74
75        // match order of units in the string
76        units.reverse();
77
78        Ok(Self { units })
79    }
80}
81
82impl TryFrom<&str> for CompoundUnit {
83    type Error = InvalidUnit;
84
85    fn try_from(value: &str) -> Result<Self, Self::Error> {
86        value.parse()
87    }
88}
89
90impl CompoundUnit {
91    /// If this derived unit can be converted to the target unit, returns the conversion factor
92    /// between them.
93    pub fn conversion_factor(&self, target: &CompoundUnit) -> Result<f64, ConversionError> {
94        let mut factor = 1.0;
95        for unit in &self.units {
96            // this is simple and elegant... and it's wrong lol
97            // let target_unit = target.units.iter()
98            //     .find(|u| std::mem::discriminant(&u.quantity) == std::mem::discriminant(&unit.quantity))
99            //     .ok_or(ConversionError { unit: *unit, target: target.units[0] })?;
100            let target_unit_factor = target.units.iter()
101                .flat_map(|u| unit.conversion_factor(*u))
102                .next()
103                .ok_or(ConversionError { unit: *unit, target: target.units[0] })?;
104            factor *= target_unit_factor;
105        }
106        Ok(factor)
107    }
108}
109
110/// Provides copy-pasteable impls for [`Unit`]s.
111macro_rules! unit_impl {
112    (
113        $doc:literal,
114        $enum_name:ident, $base_variant:ident: $base_abbr:literal $(=> $to_base_factor:literal $base_quantity:ident^$base_power:literal)?,
115        $(
116            $($variant_doc:literal,)? $variant:ident: $main_abbr:literal $(, $alt_abbr:literal)* => $factor:literal
117        ),*
118        $(,)?
119    ) => {
120        #[doc = $doc]
121        ///
122        /// The listed abbreviations are the abbreviations used to parse the unit with
123        /// [`FromStr`]. The main abbreviation comes first, followed by any alternate
124        /// abbreviations that can be used.
125        ///
126        /// The conversions are listed in terms of the [base unit]. The base unit for
127        #[doc = concat!("[`", stringify!($enum_name), "`] is [`", stringify!($enum_name), "::", stringify!($base_variant), "`].")]
128        ///
129        /// [base unit]: Convert::BASE
130        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
131        #[non_exhaustive]
132        pub enum $enum_name {
133            $(
134                $(
135                    #[doc = $variant_doc]
136                    ///
137                )?
138                #[doc = concat!("- Abbreviation: `", $main_abbr, "`", $(", `", $alt_abbr, "`")*)]
139                ///
140                #[doc = concat!("- `1 ", $main_abbr, " = ", $factor, " ", $base_abbr, "`")]
141                $variant,
142            )*
143        }
144
145        impl FromStr for $enum_name {
146            type Err = InvalidUnit;
147
148            fn from_str(value: &str) -> Result<Self, Self::Err> {
149                match value {
150                    $(
151                        $main_abbr $(| $alt_abbr)* => Ok($enum_name::$variant),
152                    )*
153                    _ => Err(InvalidUnit { unit: value.to_owned() }),
154                }
155            }
156        }
157
158        impl Display for $enum_name {
159            fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
160                match self {
161                    $(
162                        $enum_name::$variant => write!(f, $main_abbr),
163                    )*
164                }
165            }
166        }
167
168        impl Convert for $enum_name {
169            const BASE: Self = $enum_name::$base_variant;
170
171            fn conversion_factor(&self) -> f64 {
172                match self {
173                    $(
174                        $enum_name::$variant => $factor,
175                    )*
176                }
177            }
178
179            $(
180                fn conversion_factor_to(&self, target: impl Into<Unit>) -> Option<f64> {
181                    let target = target.into();
182                    if matches!(target.base, Base::$base_quantity(_)) && target.power == $base_power {
183                        Some(
184                            // convert from self to base unit^base power
185                            self.conversion_factor() * $to_base_factor
186
187                            // convert from base unit^base power to target
188                            * Unit::with_power($base_quantity::BASE, $base_power).conversion_factor(target).unwrap()
189                        )
190                    } else {
191                        None
192                    }
193                }
194            )?
195        }
196
197        impl From<$enum_name> for CompoundUnit {
198            fn from(u: $enum_name) -> Self {
199                Self { units: vec![Unit::from(u)] }
200            }
201        }
202
203        impl From<$enum_name> for Unit {
204            fn from(u: $enum_name) -> Self {
205                Self::new(Base::$enum_name(u))
206            }
207        }
208
209        impl From<$enum_name> for Base {
210            fn from(u: $enum_name) -> Self {
211                Self::$enum_name(u)
212            }
213        }
214
215        impl TryFrom<&str> for $enum_name {
216            type Error = InvalidUnit;
217
218            fn try_from(value: &str) -> Result<Self, Self::Error> {
219                value.parse()
220            }
221        }
222
223        impl $enum_name {
224            /// Creates a [`Unit`] with this quantity type and specified power.
225            pub fn pow(&self, power: i8) -> Unit {
226                Unit::with_power(Base::$enum_name(*self), power)
227            }
228
229            /// Creates a [`Unit`] with this quantity type and power 2.
230            pub fn squared(&self) -> Unit {
231                self.pow(2)
232            }
233
234            /// Creates a [`Unit`] with this quantity type and power 3.
235            pub fn cubed(&self) -> Unit {
236                self.pow(3)
237            }
238        }
239    }
240}
241
242/// A unit of measurement that includes a power.
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244#[non_exhaustive]
245pub struct Unit {
246    /// The base unit.
247    base: Base,
248
249    /// The power of the base unit.
250    power: i8,
251}
252
253impl Display for Unit {
254    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
255        write!(f, "{}", self.base)?;
256        if self.power != 1 {
257            write!(f, "^{}", self.power)?;
258        }
259        Ok(())
260    }
261}
262
263/// Try a to convert a string to a [`Unit`].
264///
265/// The string should be in the format `<base>[^<power>]`, where `<quantity>` is the
266/// abbreviation of the quantity (see [`Base`]), and `<power>` is a signed integer representing the
267/// power of the unit. If `<power>` is not specified, it is assumed to be 1.
268impl FromStr for Unit {
269    type Err = InvalidUnit;
270
271    fn from_str(value: &str) -> Result<Self, Self::Err> {
272        let mut iter = value.split('^');
273        let (a, b) = (iter.next(), iter.next());
274        if iter.next().is_some() {
275            return Err(InvalidUnit { unit: value.to_owned() });
276        }
277
278        match (a, b) {
279            (Some(a), Some(b)) => {
280                let quantity = Base::try_from(a)?;
281                let power = b.parse().map_err(|_| InvalidUnit { unit: value.to_owned() })?;
282                Ok(Unit::with_power(quantity, power))
283            },
284            (Some(a), None) => Ok(Base::try_from(a)?.into()),
285            _ => Err(InvalidUnit { unit: value.to_owned() }),
286        }
287    }
288}
289
290/// Try a to convert a string to a [`Unit`].
291///
292/// The string should be in the format `<quantity>[^<power>]`, where `<quantity>` is the
293/// abbreviation of the quantity (see [`Base`]), and `<power>` is the power of the unit. If
294/// `<power>` is not specified, it is assumed to be 1.
295impl TryFrom<&str> for Unit {
296    type Error = InvalidUnit;
297
298    fn try_from(value: &str) -> Result<Self, Self::Error> {
299        value.parse()
300    }
301}
302
303impl From<Unit> for CompoundUnit {
304    fn from(u: Unit) -> Self {
305        Self { units: vec![u] }
306    }
307}
308
309impl Unit {
310    /// Creates a new unit with the given quantity, with power 1.
311    pub fn new(quantity: impl Into<Base>) -> Self {
312        Self { base: quantity.into(), power: 1 }
313    }
314
315    /// Creates a new unit with the given quantity and power.
316    pub fn with_power(quantity: impl Into<Base>, power: i8) -> Self {
317        Self { base: quantity.into(), power }
318    }
319
320    /// If this unit can be converted to the target unit, returns the conversion factor between
321    /// them.
322    pub fn conversion_factor(&self, target: Unit) -> Result<f64, ConversionError> {
323        if self.power != target.power {
324            return self.base.conversion_factor_to(target)
325                .or_else(|| target.base.conversion_factor_to(*self).map(|f| 1.0 / f))
326                .ok_or(ConversionError { unit: *self, target });
327        }
328
329        let power = self.power as i32;
330        match (self.base, target.base) {
331            (Base::Length(l1), Base::Length(l2)) => {
332                Ok(l1.conversion_factor().powi(power)
333                    / l2.conversion_factor().powi(power))
334            },
335            (Base::Mass(m1), Base::Mass(m2)) => {
336                Ok(m1.conversion_factor().powi(power)
337                    / m2.conversion_factor().powi(power))
338            },
339            (Base::Area(a1), Base::Area(a2)) => {
340                Ok(a1.conversion_factor().powi(power)
341                    / a2.conversion_factor().powi(power))
342            },
343            (Base::Volume(v1), Base::Volume(v2)) => {
344                Ok(v1.conversion_factor().powi(power)
345                    / v2.conversion_factor().powi(power))
346            },
347            (Base::Time(t1), Base::Time(t2)) => {
348                Ok(t1.conversion_factor().powi(power)
349                    / t2.conversion_factor().powi(power))
350            },
351            _ => Err(ConversionError { unit: *self, target }),
352        }
353    }
354}
355
356/// Error returned if a unit cannot be converted to another.
357#[derive(Debug)]
358pub struct ConversionError {
359    /// The unit that could not be converted.
360    unit: Unit,
361
362    /// The target unit.
363    target: Unit,
364}
365
366impl Display for ConversionError {
367    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
368        write!(f,
369            "cannot convert from `{}` to `{}`",
370            self.unit, self.target
371        )
372    }
373}
374
375impl Error for ConversionError {}
376
377/// Error returned if the given unit abbreviation is invalid.
378#[derive(Debug)]
379pub struct InvalidUnit {
380    /// The invalid unit abbreviation.
381    unit: String,
382}
383
384impl Display for InvalidUnit {
385    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
386        write!(f, "not a valid unit: `{}`", self.unit)
387    }
388}
389
390impl Error for InvalidUnit {}
391
392/// A base unit with no power.
393///
394/// See the corresponding enum variants for the available units and their abbreviations.
395#[derive(Clone, Copy, Debug, PartialEq, Eq)]
396#[non_exhaustive]
397pub enum Base {
398    Length(Length),
399    Mass(Mass),
400    Area(Area),
401    Volume(Volume),
402    Time(Time),
403}
404
405impl Display for Base {
406    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
407        match self {
408            Base::Length(l) => write!(f, "{}", l),
409            Base::Mass(m) => write!(f, "{}", m),
410            Base::Area(a) => write!(f, "{}", a),
411            Base::Volume(v) => write!(f, "{}", v),
412            Base::Time(t) => write!(f, "{}", t),
413        }
414    }
415}
416
417/// Try a to convert a string to a [`Base`].
418///
419/// Note that [`Base`] does not encode the power of the unit, so this function will return an
420/// error if the string contains a power. Use [`Unit::try_from`] to convert a string to a unit,
421/// while accounting for the power.
422impl FromStr for Base {
423    type Err = InvalidUnit;
424
425    fn from_str(value: &str) -> Result<Self, Self::Err> {
426        Length::try_from(value).map(Base::Length)
427            .or_else(|_| Mass::try_from(value).map(Base::Mass))
428            .or_else(|_| Area::try_from(value).map(Base::Area))
429            .or_else(|_| Volume::try_from(value).map(Base::Volume))
430            .or_else(|_| Time::try_from(value).map(Base::Time))
431    }
432}
433
434/// Try a to convert a string to a [`Base`].
435///
436/// Note that [`Base`] does not encode the power of the unit, so this function will return an
437/// error if the string contains a power. Use [`Unit::try_from`] to convert a string to a unit,
438impl TryFrom<&str> for Base {
439    type Error = InvalidUnit;
440
441    fn try_from(value: &str) -> Result<Self, Self::Error> {
442        value.parse()
443    }
444}
445
446impl From<Base> for Unit {
447    fn from(q: Base) -> Self {
448        Self::new(q)
449    }
450}
451
452impl Base {
453    fn conversion_factor_to(&self, target: impl Into<Unit>) -> Option<f64> {
454        let target = target.into();
455        match self {
456            Base::Length(l) => l.conversion_factor_to(target),
457            Base::Mass(m) => m.conversion_factor_to(target),
458            Base::Area(a) => a.conversion_factor_to(target),
459            Base::Volume(v) => v.conversion_factor_to(target),
460            Base::Time(t) => t.conversion_factor_to(target),
461        }
462    }
463
464    /// Creates a [`Unit`] with this quantity type and specified power.
465    pub fn pow(&self, power: i8) -> Unit {
466        Unit::with_power(*self, power)
467    }
468
469    /// Creates a [`Unit`] with this quantity type and power 2.
470    pub fn squared(&self) -> Unit {
471        self.pow(2)
472    }
473
474    /// Creates a [`Unit`] with this quantity type and power 3.
475    pub fn cubed(&self) -> Unit {
476        self.pow(3)
477    }
478}
479
480unit_impl!("A unit of length.",
481    Length, Meter: "m",
482    Parsec: "pc" => 3.085677581e16,
483    LightYear: "ly" => 9.4607304725808e15,
484    AstronomicalUnit: "au" => 1.495978707e11,
485    NauticalMile: "nmi" => 1852.0,
486    Kilometer: "km" => 1000.0,
487    Meter: "m" => 1.0,
488    Decimeter: "dm" => 0.1,
489    Centimeter: "cm" => 0.01,
490    Millimeter: "mm" => 0.001,
491    Micrometer: "µm", "um" => 1e-6,
492    Nanometer: "nm" => 1e-9,
493    Angstrom: "Å", "A" => 1e-10,
494    Picometer: "pm" => 1e-12,
495    Mile: "mi" => 1609.344,
496    Yard: "yd" => 0.9144,
497    Foot: "ft" => 0.3048,
498    Inch: "in" => 0.0254,
499);
500
501unit_impl!("A unit of mass.",
502    Mass, Kilogram: "kg",
503    "A US (short) ton (2000 pounds).", ShortTon: "tn" => 907.18474,
504    "An imperial (long) ton (2240 pounds).", LongTon: "lt" => 1016.0469088,
505    Pound: "lb" => 0.45359237,
506    Ounce: "oz" => 0.028349523125,
507    "A metric tonne (1000 kilograms).", Tonne: "t" => 1000.0,
508    Kilogram: "kg" => 1.0,
509    Gram: "g" => 0.001,
510    Centigram: "cg" => 1e-5,
511    Milligram: "mg" => 1e-6,
512    Microgram: "µg", "ug" => 1e-9,
513    "An atomic mass unit (1/12 of the mass of a carbon 12 atom).", AtomicMassUnit: "amu" => 1.66053906892e-27,
514);
515
516unit_impl!("A unit of area.\n\nMeasurements of area are of the same kind as measurements of length with power 2. Thus, any measurement created with an area unit can be converted to a a length unit squared, and vice versa.",
517    Area, Are: "a" => 100.0 Length^2, // 1 are = 100 m^2
518    Hectare: "ha" => 100.0,
519    Decare: "daa" => 10.0,
520    Are: "a" => 1.0,
521    Deciare: "da" => 0.1,
522    Centiare: "ca" => 0.01,
523    Barn: "b" => 1e-30,
524    Acre: "ac" => 40.468564224,
525);
526
527unit_impl!("A unit of volume.\n\nMeasurements of volume are of the same kind as measurements of length with power 3. Thus, any measurement created with a volume unit can be converted to a a length unit cubed, and vice versa.",
528    Volume, Liter: "L" => 0.001 Length^3, // 1 L = 0.001 m^3
529    "A US bushel (2150.42 cubic inches).", Bushel: "bu" => 35.2390704,
530    "A US gallon (231 cubic inches).", Gallon: "gal" => 3.785411784,
531    "A US quart (57.75 cubic inches).", Quart: "qt" => 0.946352946,
532    "A US pint (28.875 cubic inches).", Pint: "pt" => 0.473176473,
533    "A US cup (8 fluid ounces).", Cup: "c" => 0.2365882365,
534    "A US fluid ounce (1.8046875 cubic inches).", FluidOunce: "floz" => 0.0295735295625,
535    "A US tablespoon (0.90234375 cubic inches).", Tablespoon: "tbsp" => 0.01478676478125,
536    "A US teaspoon (0.30078125 cubic inches).", Teaspoon: "tsp" => 0.00492892159375,
537    Kiloliter: "kL" => 1000.0,
538    Liter: "L" => 1.0,
539    Centiliter: "cL" => 0.01,
540    Milliliter: "mL" => 0.001,
541    Microliter: "µL", "uL" => 1e-6,
542);
543
544unit_impl!("A unit of time.",
545    Time, Second: "s",
546    "100 years, each of 365.25 days.", Century: "cen" => 3.15576e9,
547    "10 years, each of 365.25 days.", Decade: "dec" => 3.15576e8,
548    "A year of 365.25 days. Decades and centuries are defined in terms of this unit.", Year: "yr" => 3.15576e7,
549    Week: "wk" => 604800.0,
550    Day: "day" => 86400.0,
551    Hour: "hr" => 3600.0,
552    Minute: "min" => 60.0,
553    Second: "s" => 1.0,
554    Decisecond: "ds" => 0.1,
555    Centisecond: "cs" => 0.01,
556    Millisecond: "ms" => 0.001,
557    Microsecond: "µs", "us" => 1e-6,
558    Nanosecond: "ns" => 1e-9,
559    Picosecond: "ps" => 1e-12,
560);