Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_units/src/quantity.rs

8.2 KiB, 6 runs

created by r1870400018:17298, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Dimension-checked physical quantities.
2//!
3//! A [`Quantity`] carries a magnitude in SI base units, a count of significant
4//! figures, and a [`Dimension`]. Its multiplication and division combine both
5//! magnitude and dimension, so a velocity times a time yields a length without
6//! any further declaration. Its addition and subtraction require the two
7//! operands to share a dimension and return an [`Outcome`], erroring with both
8//! dimensions named when they differ. That error is the check that catches a
9//! physics mistake, such as adding a length to a time, at the point the code is
10//! written.
11//!
12//! Significant figures propagate through the algebra: multiplication and
13//! division keep the smaller of the two figure counts, while addition and
14//! subtraction work by decimal place, keeping the coarser of the two least
15//! significant places.
16
17use crate::dimension::Dimension;
18
19use oxedyne_fe2o3_core::prelude::*;
20use oxedyne_fe2o3_num::float;
21
22/// The value of one electronvolt in joules, exact by the 2019 SI redefinition.
23pub const ELECTRONVOLT_J: f64 = 1.602176634e-19;
24
25/// A physical quantity: a magnitude in SI base units, a significant figure
26/// count, and a dimension.
27#[derive(Clone, Copy, Debug)]
28pub struct Quantity {
29 val: f64, // Magnitude in coherent SI base units.
30 sf: u8, // Significant figures.
31 dim: Dimension, // Physical dimension.
32}
33
34impl Quantity {
35
36 /// Creates a quantity from a magnitude, significant figure count and
37 /// dimension. Errors when the figure count is zero.
38 pub fn new(val: f64, sf: u8, dim: Dimension) -> Outcome<Self> {
39 if sf == 0 {
40 return Err(err!(
41 "Number of significant figures must be > 0.";
42 Input, Invalid));
43 }
44 Ok(Self { val, sf, dim })
45 }
46
47 /// The magnitude in SI base units.
48 pub fn val(&self) -> f64 { self.val }
49
50 /// The significant figure count.
51 pub fn sf(&self) -> u8 { self.sf }
52
53 /// The dimension.
54 pub fn dim(&self) -> Dimension { self.dim }
55
56 /// Returns the magnitude rounded to its tracked significant figures, with
57 /// a tie taken away from zero.
58 ///
59 /// The figures counted are those of the magnitude, so a sign changes
60 /// nothing about which digits survive: a slope of -0.475 to two figures is
61 /// -0.48, exactly as 0.475 is 0.48. A zero carries no scale and rounds to
62 /// zero.
63 pub fn rounded(&self) -> f64 {
64 float::round_to_sf(self.val, self.sf)
65 }
66
67 /// The decimal place of the least significant digit, i.e. the power of ten
68 /// of the last figure that is still significant. A larger value means a
69 /// coarser measurement.
70 fn least_place(&self) -> i32 {
71 if self.val == 0.0 {
72 return 0; // A bare zero carries no scale.
73 }
74 (self.val.abs().log10().floor() as i32) - (self.sf as i32 - 1)
75 }
76
77 /// Multiplies two quantities, combining magnitude and dimension and keeping
78 /// the smaller significant figure count.
79 pub fn mul(&self, other: &Self) -> Self {
80 Self {
81 val: self.val * other.val,
82 sf: self.sf.min(other.sf),
83 dim: self.dim.mul(&other.dim),
84 }
85 }
86
87 /// Divides one quantity by another, combining magnitude and dimension and
88 /// keeping the smaller significant figure count.
89 pub fn div(&self, other: &Self) -> Self {
90 Self {
91 val: self.val / other.val,
92 sf: self.sf.min(other.sf),
93 dim: self.dim.div(&other.dim),
94 }
95 }
96
97 /// Raises a quantity to an integer power, combining magnitude and dimension.
98 /// The significant figure count is preserved.
99 pub fn powi(&self, n: i32) -> Self {
100 Self {
101 val: self.val.powi(n),
102 sf: self.sf,
103 dim: self.dim.powi(n),
104 }
105 }
106
107 /// Adds two quantities, requiring equal dimensions. Errors with both
108 /// dimensions named on a mismatch. The result's significant figures follow
109 /// the decimal place rule: the coarser of the two least significant places
110 /// bounds the sum.
111 pub fn add(&self, other: &Self) -> Outcome<Self> {
112 if self.dim != other.dim {
113 return Err(err!(
114 "Cannot add quantities of differing dimension: {} vs {}.",
115 self.dim, other.dim;
116 Input, Invalid, Mismatch));
117 }
118 let val = self.val + other.val;
119 Ok(Self {
120 val,
121 sf: Self::sum_sf(self, other, val),
122 dim: self.dim,
123 })
124 }
125
126 /// Subtracts one quantity from another, requiring equal dimensions. Errors
127 /// with both dimensions named on a mismatch, following the same decimal
128 /// place rule for significant figures as [`Quantity::add`].
129 pub fn sub(&self, other: &Self) -> Outcome<Self> {
130 if self.dim != other.dim {
131 return Err(err!(
132 "Cannot subtract quantities of differing dimension: {} vs {}.",
133 self.dim, other.dim;
134 Input, Invalid, Mismatch));
135 }
136 let val = self.val - other.val;
137 Ok(Self {
138 val,
139 sf: Self::sum_sf(self, other, val),
140 dim: self.dim,
141 })
142 }
143
144 /// Derives the significant figures of a sum or difference from the decimal
145 /// place rule. The result is significant only as far as the coarser of the
146 /// two inputs' least significant places.
147 fn sum_sf(a: &Self, b: &Self, result: f64) -> u8 {
148 if result == 0.0 {
149 return a.sf.min(b.sf); // No magnitude to count against.
150 }
151 let place = a.least_place().max(b.least_place());
152 let most = result.abs().log10().floor() as i32; // Place of leading digit.
153 let sf = most - place + 1;
154 if sf < 1 { 1 } else { sf as u8 }
155 }
156
157 // Constructors in SI base units. //
158
159 /// A length in metres.
160 pub fn metres(val: f64, sf: u8) -> Outcome<Self> {
161 Self::new(val, sf, Dimension::length())
162 }
163
164 /// A mass in kilograms.
165 pub fn kilograms(val: f64, sf: u8) -> Outcome<Self> {
166 Self::new(val, sf, Dimension::mass())
167 }
168
169 /// A duration in seconds.
170 pub fn seconds(val: f64, sf: u8) -> Outcome<Self> {
171 Self::new(val, sf, Dimension::time())
172 }
173
174 /// An energy in joules.
175 pub fn joules(val: f64, sf: u8) -> Outcome<Self> {
176 Self::new(val, sf, Dimension::energy())
177 }
178
179 /// A plane angle in radians, tagged distinct from a dimensionless number.
180 pub fn radians(val: f64, sf: u8) -> Outcome<Self> {
181 Self::new(val, sf, Dimension::angle())
182 }
183
184 /// A dimensionless quantity.
185 pub fn dimensionless(val: f64, sf: u8) -> Outcome<Self> {
186 Self::new(val, sf, Dimension::dimensionless())
187 }
188
189 // Common non-SI units used in teaching, each converted to SI base units at
190 // construction so that the rest of the algebra never sees them. //
191
192 /// A plane angle in degrees, converted to radians. Half a turn, 180
193 /// degrees, is π radians.
194 pub fn degrees(val: f64, sf: u8) -> Outcome<Self> {
195 Self::new(val * std::f64::consts::PI / 180.0, sf, Dimension::angle())
196 }
197
198 /// A volume in litres, converted to cubic metres.
199 pub fn litres(val: f64, sf: u8) -> Outcome<Self> {
200 Self::new(val * 1.0e-3, sf, Dimension::volume())
201 }
202
203 /// An energy in electronvolts, converted to joules.
204 pub fn electronvolts(val: f64, sf: u8) -> Outcome<Self> {
205 Self::new(val * ELECTRONVOLT_J, sf, Dimension::energy())
206 }
207
208 /// A duration in minutes, converted to seconds.
209 pub fn minutes(val: f64, sf: u8) -> Outcome<Self> {
210 Self::new(val * 60.0, sf, Dimension::time())
211 }
212
213 /// A duration in hours, converted to seconds.
214 pub fn hours(val: f64, sf: u8) -> Outcome<Self> {
215 Self::new(val * 3600.0, sf, Dimension::time())
216 }
217}
218
219impl std::ops::Mul for Quantity {
220 type Output = Quantity;
221 fn mul(self, other: Quantity) -> Quantity {
222 Quantity::mul(&self, &other)
223 }
224}
225
226impl std::ops::Div for Quantity {
227 type Output = Quantity;
228 fn div(self, other: Quantity) -> Quantity {
229 Quantity::div(&self, &other)
230 }
231}
232
233impl std::fmt::Display for Quantity {
234 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235 write!(f, "{} {}", self.rounded(), self.dim)
236 }
237}