quantity.rs (7528B)
1 //! Non-negative measured quantities and deterministic unit conversion. 2 3 use core::fmt; 4 5 use crate::Decimal; 6 use crate::unit::{ConvertError, Unit, convert_unit_decimal}; 7 8 #[cfg(not(feature = "std"))] 9 use alloc::string::String; 10 #[cfg(feature = "std")] 11 use std::string::String; 12 13 #[cfg_attr(feature = "serde", derive(serde::Serialize))] 14 #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] 15 #[cfg_attr(all(test, feature = "std"), dto(export))] 16 #[derive(Clone, Debug, PartialEq, Eq)] 17 pub struct Quantity { 18 #[cfg_attr(feature = "serde", serde(with = "crate::serde_ext::decimal_str"))] 19 #[cfg_attr(all(test, feature = "std"), dto(as = "string"))] 20 amount: Decimal, 21 unit: Unit, 22 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] 23 label: Option<String>, 24 } 25 26 impl Quantity { 27 #[inline] 28 pub fn try_new(amount: Decimal, unit: Unit) -> Result<Self, Error> { 29 if amount.is_sign_negative() && !amount.is_zero() { 30 return Err(Error::NegativeAmount); 31 } 32 Ok(Self { 33 amount: if amount.is_zero() { 34 Decimal::ZERO 35 } else { 36 amount 37 }, 38 unit, 39 label: None, 40 }) 41 } 42 43 #[inline] 44 pub const fn amount(&self) -> Decimal { 45 self.amount 46 } 47 48 #[inline] 49 pub const fn unit(&self) -> Unit { 50 self.unit 51 } 52 53 #[inline] 54 pub fn label(&self) -> Option<&str> { 55 self.label.as_deref() 56 } 57 58 #[inline] 59 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self { 60 self.label = Some(label.into()); 61 self 62 } 63 64 #[inline] 65 pub fn with_optional_label<S: Into<String>>(mut self, label: Option<S>) -> Self { 66 self.label = label.map(|s| s.into()); 67 self 68 } 69 70 #[inline] 71 pub fn clear_label(mut self) -> Self { 72 self.label = None; 73 self 74 } 75 76 #[inline] 77 pub fn zero(unit: Unit) -> Self { 78 Self { 79 amount: Decimal::ZERO, 80 unit, 81 label: None, 82 } 83 } 84 85 #[inline] 86 pub fn is_zero(&self) -> bool { 87 self.amount.is_zero() 88 } 89 90 #[inline] 91 pub fn is_canonical(&self) -> bool { 92 self.unit == self.unit.canonical_unit() 93 } 94 95 #[inline] 96 pub fn canonical_unit(&self) -> Unit { 97 self.unit.canonical_unit() 98 } 99 100 #[inline] 101 pub fn try_convert_to(&self, unit: Unit) -> Result<Quantity, ConvertError> { 102 if self.unit == unit { 103 return Ok(self.clone()); 104 } 105 let amount = convert_unit_decimal(self.amount, self.unit, unit)?; 106 Ok(Quantity { 107 amount, 108 unit, 109 label: self.label.clone(), 110 }) 111 } 112 113 #[inline] 114 pub fn to_canonical(&self) -> Result<Quantity, ConvertError> { 115 self.try_convert_to(self.unit.canonical_unit()) 116 } 117 118 #[inline] 119 pub(crate) fn ensure_non_negative(&self) -> Result<(), Error> { 120 if self.amount.is_sign_negative() && !self.amount.is_zero() { 121 return Err(Error::NegativeAmount); 122 } 123 Ok(()) 124 } 125 126 #[inline] 127 pub fn try_add(&self, rhs: &Quantity) -> Result<Quantity, Error> { 128 if self.unit != rhs.unit { 129 return Err(Error::UnitMismatch); 130 } 131 Ok(Quantity { 132 amount: self.amount.checked_add(rhs.amount)?, 133 unit: self.unit, 134 label: self.label.clone(), 135 }) 136 } 137 138 #[inline] 139 pub fn try_sub(&self, rhs: &Quantity) -> Result<Quantity, Error> { 140 if self.unit != rhs.unit { 141 return Err(Error::UnitMismatch); 142 } 143 let amount = self.amount.checked_sub(rhs.amount)?; 144 if amount.is_sign_negative() { 145 return Err(Error::NegativeAmount); 146 } 147 Ok(Quantity { 148 amount, 149 unit: self.unit, 150 label: self.label.clone(), 151 }) 152 } 153 154 pub fn checked_add(&self, rhs: &Quantity) -> Option<Quantity> { 155 self.try_add(rhs).ok() 156 } 157 158 pub fn checked_sub(&self, rhs: &Quantity) -> Option<Quantity> { 159 self.try_sub(rhs).ok() 160 } 161 162 #[inline] 163 pub fn checked_mul_decimal(&self, factor: Decimal) -> Result<Quantity, Error> { 164 let amount = self.amount.checked_mul(factor)?; 165 if amount.is_sign_negative() { 166 return Err(Error::NegativeAmount); 167 } 168 Ok(Quantity { 169 amount, 170 unit: self.unit, 171 label: self.label.clone(), 172 }) 173 } 174 175 #[inline] 176 pub fn checked_div_decimal(&self, divisor: Decimal) -> Result<Quantity, Error> { 177 let amount = self.amount.checked_div(divisor)?; 178 if amount.is_sign_negative() { 179 return Err(Error::NegativeAmount); 180 } 181 Ok(Quantity { 182 amount, 183 unit: self.unit, 184 label: self.label.clone(), 185 }) 186 } 187 } 188 189 #[cfg(test)] 190 mod tests { 191 use super::*; 192 193 fn negative_zero() -> Decimal { 194 Decimal::from_backend(rust_decimal::Decimal::from_parts(0, 0, 0, true, 0)) 195 } 196 197 #[test] 198 fn internal_nonnegative_invariant_covers_invalid_and_signed_zero_states() { 199 let invalid = Quantity { 200 amount: Decimal::from_backend(rust_decimal::Decimal::from_parts(1, 0, 0, true, 0)), 201 unit: Unit::Each, 202 label: None, 203 }; 204 assert_eq!(invalid.ensure_non_negative(), Err(Error::NegativeAmount)); 205 206 let canonical = Quantity::try_new(negative_zero(), Unit::Each).unwrap(); 207 assert_eq!(canonical.amount(), Decimal::ZERO); 208 assert_eq!(canonical.ensure_non_negative(), Ok(())); 209 } 210 } 211 212 #[cfg(feature = "serde")] 213 impl<'de> serde::Deserialize<'de> for Quantity { 214 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { 215 #[derive(serde::Deserialize)] 216 struct Wire { 217 #[serde(with = "crate::serde_ext::decimal_str")] 218 amount: Decimal, 219 unit: Unit, 220 #[serde(default)] 221 label: Option<String>, 222 } 223 224 let wire = Wire::deserialize(deserializer)?; 225 Self::try_new(wire.amount, wire.unit) 226 .map(|quantity| quantity.with_optional_label(wire.label)) 227 .map_err(serde::de::Error::custom) 228 } 229 } 230 231 impl fmt::Display for Quantity { 232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 233 write!(f, "{} {}", self.amount.normalize(), self.unit)?; 234 if let Some(label) = &self.label { 235 write!(f, " ({label})")?; 236 } 237 Ok(()) 238 } 239 } 240 241 #[non_exhaustive] 242 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 243 pub enum Error { 244 NegativeAmount, 245 UnitMismatch, 246 ArithmeticOverflow, 247 DivisionByZero, 248 } 249 250 impl fmt::Display for Error { 251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 252 match self { 253 Error::NegativeAmount => { 254 write!(f, "quantity amount must be ≥ 0") 255 } 256 Error::UnitMismatch => { 257 write!(f, "quantity unit mismatch") 258 } 259 Error::ArithmeticOverflow => write!(f, "quantity arithmetic overflow"), 260 Error::DivisionByZero => write!(f, "quantity division by zero"), 261 } 262 } 263 } 264 265 #[cfg(feature = "std")] 266 impl std::error::Error for Error {} 267 268 impl From<crate::decimal::Error> for Error { 269 fn from(error: crate::decimal::Error) -> Self { 270 match error { 271 crate::decimal::Error::DivisionByZero => Self::DivisionByZero, 272 _ => Self::ArithmeticOverflow, 273 } 274 } 275 }