dimensional_analyser/
quantity.rs

1//! Provides dimensional arithmetic and conversion between physical [`Quantity`]'s
2//! expressed in arbitrary units. Built on top of [`Dimension`].
3use crate::{debug_println};
4use crate::dimension::{ConversionExponentError, DIMENSIONLESS, Dimension, UnconvertableDimensionsError, DimensionalAnalysable};
5use core::fmt;
6use std::fmt::{Display, Formatter, LowerExp};
7use std::iter::Product;
8use std::ops::{Add, Div, Mul, Sub};
9use std::error::Error;
10
11
12/// Error when a single [`Quantity`] can't be converted to a [`Dimension`].
13#[derive(Debug, Clone, PartialEq)]
14pub struct UnconvertableQuantityError {
15    base_quantity: Quantity,
16    conversion_exponent_error: ConversionExponentError,
17}
18impl Display for UnconvertableQuantityError {
19    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
20        let Self { base_quantity, conversion_exponent_error } = self;
21        write!(f, "Failed to convert {base_quantity}. {conversion_exponent_error}")
22    }
23}
24impl Error for UnconvertableQuantityError {}
25
26/// Error when multiple [`Quantity`]s can't be converted to a [`Dimension`].
27#[derive(Debug, Clone, PartialEq)]
28pub struct UnconvertableQuantitiesError {
29    base_quantities: Vec<Quantity>,
30    dimension_error: UnconvertableDimensionsError,
31}
32impl Display for UnconvertableQuantitiesError {
33    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
34        let Self { base_quantities, dimension_error } = self;
35        write!(f, "Failed to convert {}. {dimension_error}", Quantities(base_quantities))
36    }
37}
38impl Error for UnconvertableQuantitiesError {}
39
40
41/// Error when both [`Dimension`]s are incompatible.
42#[derive(Debug, Clone, PartialEq)]
43pub struct DifferentDimensionError {
44    left_dimension: Dimension,
45    right_dimension: Dimension,
46}
47impl Display for DifferentDimensionError {
48    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
49        let Self { left_dimension, right_dimension } = self;
50        write!(f, "Uncompatible dimensions: {left_dimension} and {right_dimension}")
51    }
52}
53impl Error for DifferentDimensionError {}
54
55/// Distinguishes the different ways [`Quantity`]s are related to one another.
56#[derive(PartialEq, Debug)]
57pub enum Equality {
58    /// Same value and same dimension: `2 s = 2 s`.
59    Identical,
60    /// Equivalent multiple or submultiple: `120 s = 2 min`.
61    ScalarMultiple(f64),
62    /// Analogous implied by raising to a power: `2 s = 0.5` Hz or `10 m = 100 m^2`.
63    PowerProyection(f64),
64    /// None of the above.
65    Different,
66}
67
68/// Represents a physical quantity consisting of a scalar value and a [`Dimension`].
69/// # Example
70/// ```rust
71/// use dimensional_analyser::{quantity::Quantity, dimensions::le_systeme_international_d_unites::{JOULE, base_units::{KILOGRAM, METER, SECOND}}};
72///
73/// let mass = Quantity::new(5, &KILOGRAM);
74/// let velocity = Quantity::new(10, &(&*METER / &*SECOND));
75/// let kinetic_energy = (&mass * &velocity.power(2)) / 2;
76/// assert_eq!(kinetic_energy, Quantity::new(250, &JOULE));
77/// ```
78#[derive(Debug, Clone)]
79pub struct Quantity {
80    value: f64,
81    dimension: Dimension,
82}
83impl Quantity {
84    /// Creates a new [`Quantity`] with the given `value` and [`Dimension`].
85    pub fn new<T: Into<f64>>(value: T, dimension: &Dimension) -> Self {
86        Self { value: value.into(), dimension: dimension.clone() }
87    }
88    /// Raises the quantity to the specified power.
89    #[must_use]
90    pub fn power<T: Into<f64> + Copy>(&self, exponent: T) -> Self {
91        Self {
92            value: self.value.powf(exponent.into()),
93            dimension: self.dimension.power(exponent),
94        }
95    }
96    /// Attempts to convert the quantity to another compatible dimension.
97    /// # Errors
98    /// [`UnconvertableQuantityError`] when the base [`Quantity`] can't be converted to the target [`Dimension`]
99    /// # Example
100    /// ```rust
101    /// use dimensional_analyser::{dimension::Prefix::Centi, quantity::Quantity, dimensions::le_systeme_international_d_unites::base_units::METER};
102    ///
103    /// let one_square_centimeter = Quantity::new(1, &METER.prefix(&Centi).square());
104    /// let area_in_square_meters = one_square_centimeter.convert_to(&METER.square()).unwrap();
105    /// assert_eq!(area_in_square_meters, Quantity::new(0.0001, &METER.square()));
106    /// ```
107    pub fn convert_to(&self, other: &Dimension) -> Result<Self, UnconvertableQuantityError> {
108        Ok(Self {
109            value: (self.value * self.dimension.scaling_factor()).powf(
110                self.dimension.get_conversion_exponent(other).map_err(|conversion_exponent_error|
111                    UnconvertableQuantityError { base_quantity: self.clone(), conversion_exponent_error }
112                )?
113            ) / other.scaling_factor(),
114            dimension: other.clone(),
115        })
116    }
117    /// Returns the relationship between both [`Quantity`]'s.
118    /// # Panics
119    /// Not enough data is known after converting `self` to the `other.dimension` so an expect is used
120    /// # Example
121    /// ```rust
122    /// use dimensional_analyser::{quantity::Quantity, dimensions::le_systeme_international_d_unites::{MINUTE, base_units::SECOND}, quantity::Equality};
123    ///
124    /// let one_minute = Quantity::new(1, &MINUTE);
125    /// let sixty_seconds = Quantity::new(60, &SECOND);
126    /// match one_minute.get_equality_with(&sixty_seconds) {
127    ///     Equality::ScalarMultiple(factor) => assert_eq!(factor, 60.0),
128    ///     _ => panic!("1 min and 60 s should be scalar multiples"),
129    /// }
130    /// ```
131    #[must_use]
132    pub fn get_equality_with(&self, other: &Self) -> Equality {
133        debug_println!("Comparing {} and {}", self, other);
134        if self == other {
135            return Equality::Identical;
136        }
137        self.convert_to(&other.dimension).map_or(Equality::Different, |converted| {
138            debug_println!("Converted to: {}", converted);
139            if &converted != other {
140                Equality::Different
141            } else if [&self.dimension, &other.dimension].have_same_exponents() {
142                Equality::ScalarMultiple(self.dimension.scaling_factor() / other.dimension.scaling_factor())
143            } else {
144                let exponent = self.dimension.get_conversion_exponent(&other.dimension).expect("Should have an exponent if we got here");
145                Equality::PowerProyection(exponent)
146            }
147        })
148    }
149    /// Helper function to print how both [`Quantity`]'s are related.
150    pub fn show_comparizon_results_with(&self, other: &Self) {
151        match self.get_equality_with(other) {
152            Equality::Identical => {
153                println!("{self} and {other} are identical");
154            }
155            Equality::ScalarMultiple(factor) => {
156                println!("{self} and {other} are scalar multiples (factor {factor})");
157            }
158            Equality::PowerProyection(exponent) => {
159                println!("{self} and {other} are power symmetric (exponent {exponent})");
160            }
161            Equality::Different => {
162                println!("{self} and {other} are different dimensions");
163            }
164        }
165    }
166}
167impl PartialEq for Quantity {
168    fn eq(&self, other: &Self) -> bool {
169        (self.dimension == other.dimension) && (self.value / other.value - 1.0).abs() < f64::from(f32::EPSILON)
170    }
171}
172/// A helper struct to show multiple [`Quantity`]'s is a concise manner.
173pub struct Quantities<'a>(pub &'a Vec<Quantity>);
174impl Display for Quantities<'_> {
175    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
176        write!(f, "[{}]", self
177            .0
178            .iter()
179            .map(|d| format!("{d}"))
180            .collect::<Vec<_>>()
181            .join(", ")
182        )
183    }
184}
185impl LowerExp for Quantity {
186    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
187        let value = self.value;
188        let dimension = &self.dimension;
189        write!(f, "{value:e}[{dimension:e}]")
190    }
191}
192impl Display for Quantity {
193    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
194        let value = self.value;
195        let dimension = &self.dimension;
196        write!(f, "{value}[{dimension}]")
197    }
198}
199impl Mul for &Quantity {
200    type Output = Quantity;
201    fn mul(self, rhs: Self) -> Self::Output {
202        Quantity {
203            value: self.value * rhs.value,
204            dimension: &self.dimension * &rhs.dimension,
205        }
206    }
207}
208impl<T: Into<f64>> Mul<T> for Quantity {
209    type Output = Self;
210    fn mul(self, rhs: T) -> Self::Output {
211        Self {
212            value: self.value * rhs.into(),
213            dimension: self.dimension,
214        }
215    }
216}
217impl Div for &Quantity {
218    type Output = Quantity;
219    fn div(self, rhs: Self) -> Self::Output {
220        Quantity {
221            value: self.value / rhs.value,
222            dimension: &self.dimension / &rhs.dimension,
223        }
224    }
225}
226impl<T: Into<f64>> Div<T> for Quantity {
227    type Output = Self;
228    fn div(self, rhs: T) -> Self::Output {
229        Self {
230            value: self.value / rhs.into(),
231            dimension: self.dimension,
232        }
233    }
234}
235impl Add for &Quantity {
236    type Output = Result<Quantity, DifferentDimensionError>;
237    fn add(self, rhs: Self) -> Self::Output {
238        let exponents = [&self.dimension, &rhs.dimension].exponents();
239        if exponents[0] != exponents[1] {
240            return Err(DifferentDimensionError {
241                left_dimension: self.dimension.clone(),
242                right_dimension: rhs.dimension.clone(),
243            })
244        }
245        Ok(Quantity {
246            value: self.value + rhs.value,
247            dimension: self.dimension.clone(),
248        })
249    }
250}
251impl Sub for &Quantity {
252    type Output = Result<Quantity, DifferentDimensionError>;
253    fn sub(self, rhs: Self) -> Self::Output {
254        if self.dimension != rhs.dimension {
255            return Err(DifferentDimensionError {
256                left_dimension: self.dimension.clone(),
257                right_dimension: rhs.dimension.clone(),
258            })
259        }
260        Ok(Quantity {
261            value: self.value - rhs.value,
262            dimension: self.dimension.clone(),
263        })
264    }
265}
266impl Product for Quantity {
267    fn product<I: Iterator<Item = Self>>(iter: I) -> Self {
268        
269        iter.fold(Self::new(1.0, &DIMENSIONLESS), |acc, x| &acc * &x)
270    }
271}
272
273/// Provides dimensional analysis capabilities for [`Quantity`]'s.
274pub trait DimensionalAnalysableQuantity {
275    /// Converts [`Quantity`]'s to a [`Dimension`].
276    /// # Errors
277    /// [`UnconvertableQuantitiesError`] when the [`Quantity`]s can't be converted to a [`Dimension`] 
278    fn convert_to(&self, other: &Dimension) -> Result<Quantity, UnconvertableQuantitiesError>;
279    /// Converts [`Quantity`]'s to each [`Dimension`] separately.
280    /// # Errors
281    /// [`UnconvertableQuantitiesError`] when the [`Quantity`]s can't be converted to each [`Dimension`] 
282    fn convertable_to(&self, others: &[&Dimension]) -> Result<Box<[Quantity]>, UnconvertableQuantitiesError>;
283}
284impl DimensionalAnalysableQuantity for [&Quantity] {
285    fn convert_to(&self, other: &Dimension) -> Result<Quantity, UnconvertableQuantitiesError> {
286        let quantities: Box<[&Dimension]> = self.iter().map(|quantity| &quantity.dimension).collect();
287        let same_units: Quantity = quantities.exponents_to(other).map_err(
288            |dimension_error|UnconvertableQuantitiesError {
289                base_quantities: self.iter().copied().cloned().collect(), dimension_error
290            }
291        )?.iter().enumerate().map(|(index, &power)| self[index].power(power)).product();
292        Ok(Quantity{
293            value: same_units.value * same_units.dimension.scaling_factor() / other.scaling_factor(),
294            dimension: other.clone()
295        })
296    }
297    fn convertable_to(&self, others: &[&Dimension]) -> Result<Box<[Quantity]>, UnconvertableQuantitiesError> {
298        others.iter().map(|other| self.convert_to(other)).collect()
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use crate::{
305        debug_println, dim, dimension::Prefix::{
306                Hecto,
307                Kilo,
308                Milli,
309                Pico
310            }, dimensions::{le_systeme_international_d_unites::{
311                    HERTZ, HOUR, JOULE, MINUTE, base_units::{
312                        KILOGRAM,
313                        METER, SECOND
314                    }
315                }, the_seven_c_s::base_units::C_AS_THE_SPEED_OF_LIGHT}, quantity::*
316    };
317
318    #[test]
319    fn test_add() {
320        let lhs = Quantity::new(120, &SECOND);
321        let rhs = Quantity::new(2, &MINUTE).convert_to(&lhs.dimension).expect("Seconds and minutes are compatible");
322        let sum = &lhs + &rhs;
323        assert!(sum.is_ok());
324        let sum = sum.unwrap();
325        assert_eq!(sum, Quantity::new(240, &SECOND));
326    }
327
328    // The following tests remain in the unit test suite. A number of
329    // illustrative examples were moved to doctests in the module header
330    // to improve the crate documentation and reduce bloat in this test
331    // module.
332
333    #[test]
334    #[allow(clippy::float_cmp)]
335    fn curseder_units_optic_fiber_example() {
336        let pulse_broadening = Quantity::new(1.2, &(&SECOND.prefix(&Pico) / &METER.prefix(&Kilo).power(0.5)));
337        let propagation_distance = Quantity::new(100, &(METER.prefix(&Kilo)));
338
339        let total_spread = &pulse_broadening * &propagation_distance.power(0.5);
340        debug_println!("Total pulse spread (in picoseconds): {}", total_spread);
341        assert_eq!(total_spread.value, 12.0);
342    }
343
344    #[test]
345    fn bomb_explosion_radius_example() {
346        let energy = Quantity::new(100_000, &JOULE);
347        let explosion_time = Quantity::new(1, &SECOND);
348        let air_density = Quantity::new(1, &(&*KILOGRAM / &METER.cube()));
349        let radius = (&(&energy / &air_density) * &explosion_time.power(2.0)).convert_to(&METER).expect("Resulting dimension should be length");
350        debug_println!("Estimated explosion radius (in meters): {}", radius);
351        assert!((radius.value - 10.0).abs() < 1.0);
352    } 
353
354    #[test]
355    #[allow(clippy::float_cmp)]
356    fn complex_equalty_example() {
357        let hectareas = Quantity::new(100, &METER.prefix(&Hecto).square());
358        let length = Quantity::new(1_000_000, &METER.prefix(&Milli));
359        match length.get_equality_with(&hectareas) {
360            Equality::PowerProyection(exponent) => {
361                assert_eq!(exponent, 2.0);
362            }
363            _ => {
364                panic!("100 hectareas and 1,000,000 millimeters should be power symmetric");
365            }
366        }
367    }
368
369    #[test]
370    #[allow(clippy::float_cmp)]
371    fn another_contrived_example() {
372        let frequency = Quantity::new(2, &HERTZ.prefix(&Kilo));
373        let period = Quantity::new(0.5, &SECOND.prefix(&Milli)).power(2);
374        match frequency.get_equality_with(&period) {
375            Equality::PowerProyection(exponent) => {
376                assert_eq!(exponent, -2.0);
377            }
378            _ => {
379                panic!("2 kHz and 0.25 ms^2 should be power symmetric");
380            }
381        }
382    }
383    
384    #[test]
385    fn incompatible_addition_example() {
386        let length = Quantity::new(1, &METER);
387        let time = Quantity::new(1, &SECOND);
388        let result = &length + &time;
389        assert!(result.is_err());
390        debug_println!("Error message: {}", result.err().unwrap());
391    }
392    
393    #[test]
394    fn incompatible_conversion_example() {
395        let length = Quantity::new(1, &(&*METER / &*SECOND));
396        let time = &*SECOND;
397        let result = length.convert_to(time);
398        assert!(result.is_err());
399        debug_println!("Error message: {}", result.err().unwrap());
400    }
401
402    #[test]
403    fn bomb_explosion_radius_example_as_dimensional_analysis() {
404        let energy = Quantity::new(100_000, &JOULE);
405        let explosion_time = Quantity::new(1, &SECOND);
406        let air_density = Quantity::new(1, &(&*KILOGRAM / &METER.power(3)));
407        let radius = [&energy, &explosion_time, &air_density].convert_to(&METER).expect("Units to be convertible");
408        debug_println!("Estimated explosion radius (in meters): {}", radius);
409        assert!((radius.value - 10.0).abs() < 1.0);
410    } 
411
412    #[test]
413    fn unordered_but_equal() {
414        let letter = Dimension::new("letter");
415        let minute = &*MINUTE;
416        let typing_speed_a = Quantity::new(24, &(&letter * &minute.inverse()));
417        let typing_speed_b = Quantity::new(24, &(&minute.inverse() * &letter));
418        assert_eq!(typing_speed_a, typing_speed_b);
419    }
420
421    #[test]
422    fn unordered_but_identical() {
423        let letter = Dimension::new("letter");
424        let minute = &*MINUTE;
425        let typing_speed_a = Quantity::new(24, &(&letter * &minute.inverse()));
426        let typing_speed_b = Quantity::new(24, &(&minute.inverse() * &letter));
427        assert_eq!(typing_speed_a.get_equality_with(&typing_speed_b), Equality::Identical);
428    }
429
430    #[test]
431    fn unordered_scalar_multiples() {
432        let letter = Dimension::new("letter");
433        let word = letter.scale(5);
434        let minute = &*MINUTE;
435        let typing_speed_a = Quantity::new(24, &(&word * &minute.inverse()));
436        let typing_speed_b = Quantity::new(120, &(&minute.inverse() * &letter));
437        assert_eq!(typing_speed_a.get_equality_with(&typing_speed_b), Equality::ScalarMultiple(5.0));
438    }
439
440    #[test]
441    fn unordered_power_proyections() {
442        let letter = Dimension::new("letter");
443        let word = letter.scale(5);
444        let minute = &*MINUTE;
445        let typing_speed_a = Quantity::new(24, &(&word * &minute.inverse()));
446        let typing_speed_b = typing_speed_a.power(0.3);
447        assert_eq!(typing_speed_a.get_equality_with(&typing_speed_b), Equality::PowerProyection(0.3));
448    }
449
450    #[test]
451    #[allow(clippy::float_cmp)]
452    fn conversion_with_different_multipliers() {
453        let dollar = Dimension::new("dollar");
454        let money_gained = Quantity::new(40, &dollar);
455        let match_duration = Quantity::new(7, &MINUTE);
456        let salary = [&money_gained, &match_duration].convert_to(&(&dollar / &*HOUR)).expect("Convertable");
457        assert_eq!(salary.dimension, &dollar / &*HOUR);
458        assert_eq!(salary.value, 3.428_571_428_571_428e2);
459    }
460
461    /*
462    "frecuencia"	"velocidad de la luz"	    "longitud de onda"
463    540[Tera.HERTZ]	1[C_AS_THE_SPEED_OF_LIGHT]	[A2, B2] => nano .meter
464    */
465    #[test]
466    fn wavelength_from_frequency_and_the_speed_of_light() {
467        let frequency = Quantity::new(540, dim!(,1,,Tera HERTZ));
468        let speed_of_light = Quantity::new(1, dim!(C_AS_THE_SPEED_OF_LIGHT));
469        let length = dim!(,1,,Nano METER);
470        let wavelength = [&frequency, &speed_of_light]
471            .convert_to(length)
472            .expect("Should be convertable");
473        assert_eq!(wavelength, Quantity::new(555.171_218_518_518_6, length));
474    }
475}