1use 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#[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#[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#[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#[derive(PartialEq, Debug)]
57pub enum Equality {
58 Identical,
60 ScalarMultiple(f64),
62 PowerProyection(f64),
64 Different,
66}
67
68#[derive(Debug, Clone)]
79pub struct Quantity {
80 value: f64,
81 dimension: Dimension,
82}
83impl Quantity {
84 pub fn new<T: Into<f64>>(value: T, dimension: &Dimension) -> Self {
86 Self { value: value.into(), dimension: dimension.clone() }
87 }
88 #[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 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 #[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 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}
172pub 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
273pub trait DimensionalAnalysableQuantity {
275 fn convert_to(&self, other: &Dimension) -> Result<Quantity, UnconvertableQuantitiesError>;
279 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 #[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 #[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}