Skip to main content

ucum/
quantity.rs

1//! A typed quantity: a numeric magnitude paired with a UCUM unit expression,
2//! with dimensional arithmetic and conversion.
3
4use crate::error::UcumError;
5use crate::{Analysis, Dimension};
6
7/// A magnitude expressed in a UCUM unit.
8///
9/// `Quantity` keeps its unit *symbolic* (as a UCUM string). Arithmetic builds a
10/// new compound unit expression rather than eagerly reducing, so no precision is
11/// lost and any unit (even one this build doesn't know) round-trips. Errors
12/// surface lazily, when you [`analyze`](Quantity::analyze) or
13/// [`convert_to`](Quantity::convert_to).
14///
15/// ```
16/// use ucum::Quantity;
17///
18/// let force = Quantity::new(2.0, "g").mul(&Quantity::new(3.0, "m/s2"));
19/// assert_eq!(force.value, 6.0);
20/// // The product is commensurable with the newton's base form.
21/// assert!(force.is_comparable("N").unwrap());
22///
23/// // Dividing commensurable quantities yields a dimensionless ratio.
24/// let ratio = Quantity::new(1.0, "[lb_av]/h").div(&Quantity::new(1.0, "kg/s"));
25/// assert!(ratio.dimension().unwrap().is_dimensionless());
26/// ```
27#[derive(Clone, Debug, PartialEq)]
28pub struct Quantity {
29    /// The numeric magnitude.
30    pub value: f64,
31    /// The UCUM unit expression.
32    pub unit: String,
33}
34
35impl Quantity {
36    /// Create a quantity from a value and a UCUM unit string.
37    pub fn new(value: f64, unit: impl Into<String>) -> Self {
38        Quantity {
39            value,
40            unit: unit.into(),
41        }
42    }
43
44    /// Analyze this quantity's unit. Total.
45    pub fn analyze(&self) -> Result<Analysis, UcumError> {
46        crate::analyze(&self.unit)
47    }
48
49    /// The dimension of this quantity's unit. Total.
50    pub fn dimension(&self) -> Result<Dimension, UcumError> {
51        Ok(self.analyze()?.dimension)
52    }
53
54    /// Whether this quantity's unit is commensurable with `unit`. Total.
55    pub fn is_comparable(&self, unit: &str) -> Result<bool, UcumError> {
56        crate::is_comparable(&self.unit, unit)
57    }
58
59    /// Multiply two quantities: values multiply and units concatenate. The
60    /// result unit is `(self.unit).(other.unit)`.
61    pub fn mul(&self, other: &Quantity) -> Quantity {
62        Quantity {
63            value: self.value * other.value,
64            unit: format!("{}.{}", group(&self.unit), group(&other.unit)),
65        }
66    }
67
68    /// Divide two quantities: values divide and the result unit is
69    /// `(self.unit)/(other.unit)`.
70    pub fn div(&self, other: &Quantity) -> Quantity {
71        Quantity {
72            value: self.value / other.value,
73            unit: format!("{}/{}", group(&self.unit), group(&other.unit)),
74        }
75    }
76
77    /// Convert this quantity to `unit`, returning a new quantity. Total.
78    ///
79    /// Errors with [`UcumError::NotComparable`] or
80    /// [`UcumError::UnsupportedSpecial`] as for [`crate::convert`].
81    pub fn convert_to(&self, unit: &str) -> Result<Quantity, UcumError> {
82        Ok(Quantity {
83            value: crate::convert(self.value, &self.unit, unit)?,
84            unit: unit.to_string(),
85        })
86    }
87}
88
89/// Wrap a unit in parentheses for safe composition; an empty unit is the unity.
90fn group(unit: &str) -> String {
91    if unit.trim().is_empty() {
92        "1".to_string()
93    } else {
94        format!("({unit})")
95    }
96}