qualia_core_db/solvers/units/
quantity.rs1use super::dimension::Dimension;
6use super::UnitsError;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct Quantity {
11 pub value: f64,
13 pub dimension: Dimension,
14}
15
16impl Quantity {
17 pub const fn new(value: f64, dimension: Dimension) -> Self {
18 Self { value, dimension }
19 }
20
21 pub const fn scalar(value: f64) -> Self {
23 Self {
24 value,
25 dimension: Dimension::DIMENSIONLESS,
26 }
27 }
28
29 pub fn add(&self, other: &Quantity) -> Result<Quantity, UnitsError> {
31 if self.dimension != other.dimension {
32 return Err(UnitsError::IncompatibleDimensions);
33 }
34 Ok(Quantity {
35 value: self.value + other.value,
36 dimension: self.dimension,
37 })
38 }
39
40 pub fn sub(&self, other: &Quantity) -> Result<Quantity, UnitsError> {
42 if self.dimension != other.dimension {
43 return Err(UnitsError::IncompatibleDimensions);
44 }
45 Ok(Quantity {
46 value: self.value - other.value,
47 dimension: self.dimension,
48 })
49 }
50
51 pub fn mul(&self, other: &Quantity) -> Quantity {
53 Quantity {
54 value: self.value * other.value,
55 dimension: self.dimension.mul(&other.dimension),
56 }
57 }
58
59 pub fn div(&self, other: &Quantity) -> Option<Quantity> {
61 if other.value == 0.0 {
62 return None;
63 }
64 Some(Quantity {
65 value: self.value / other.value,
66 dimension: self.dimension.div(&other.dimension),
67 })
68 }
69
70 pub fn scale(&self, factor: f64) -> Quantity {
72 Quantity {
73 value: self.value * factor,
74 dimension: self.dimension,
75 }
76 }
77
78 pub fn powi(&self, n: i32) -> Quantity {
80 Quantity {
81 value: self.value.powi(n),
82 dimension: self.dimension.powi(n),
83 }
84 }
85
86 pub fn compatible_with(&self, other: &Quantity) -> bool {
88 self.dimension == other.dimension
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 const EPS: f64 = 1e-9;
96
97 fn metres(v: f64) -> Quantity {
98 Quantity::new(v, Dimension::LENGTH)
99 }
100 fn seconds(v: f64) -> Quantity {
101 Quantity::new(v, Dimension::TIME)
102 }
103
104 #[test]
105 fn adding_like_dimensions_works_unlike_fails() {
106 let total = metres(3.0).add(&metres(4.0)).unwrap();
107 assert!((total.value - 7.0).abs() < EPS);
108 assert_eq!(total.dimension, Dimension::LENGTH);
109 assert_eq!(
111 metres(1.0).add(&seconds(1.0)).unwrap_err(),
112 UnitsError::IncompatibleDimensions
113 );
114 }
115
116 #[test]
117 fn products_derive_new_dimensions() {
118 let v = metres(100.0).div(&seconds(10.0)).unwrap();
120 assert!((v.value - 10.0).abs() < EPS);
121 assert_eq!(v.dimension, Dimension::VELOCITY);
122 let force = Quantity::new(5.0, Dimension::FORCE);
124 let work = force.mul(&metres(2.0));
125 assert!((work.value - 10.0).abs() < EPS);
126 assert_eq!(work.dimension, Dimension::ENERGY);
127 }
128
129 #[test]
130 fn kinetic_energy_is_dimensionally_consistent() {
131 let m = Quantity::new(2.0, Dimension::MASS);
133 let v = Quantity::new(3.0, Dimension::VELOCITY);
134 let ke = m.mul(&v.powi(2)).scale(0.5);
135 assert!((ke.value - 9.0).abs() < EPS); assert_eq!(ke.dimension, Dimension::ENERGY);
137 }
138
139 #[test]
140 fn divide_by_zero_fails_closed() {
141 assert!(metres(1.0).div(&seconds(0.0)).is_none());
142 }
143}