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}