Skip to main content

iridium_units/
quantity.rs

1//! Physical quantities with values and units.
2
3use crate::dimension::Rational16;
4use crate::error::{UnitError, UnitResult};
5use crate::unit::base::BaseUnit;
6use crate::unit::Unit;
7use std::fmt;
8use std::ops::{Add, Div, Mul, Neg, Sub};
9
10/// A physical quantity: a numeric value with an associated unit.
11///
12/// # Arithmetic
13///
14/// - **Multiplication and division** always succeed and combine dimensions
15///   automatically (e.g., `m * m` → `m²`, `m / s` → `m·s⁻¹`).
16/// - **Addition and subtraction** via `+` and `-` **panic** if the two
17///   quantities have incompatible dimensions. Use [`checked_add`](Quantity::checked_add)
18///   and [`checked_sub`](Quantity::checked_sub) for fallible versions that
19///   return `Result`.
20///
21/// # Examples
22///
23/// ```
24/// use iridium_units::prelude::*;
25///
26/// let distance = 100.0 * M;
27/// let time = 9.58 * S;
28/// let speed = &distance / &time;
29///
30/// let speed_kmh = speed.to(&(KM / H)).unwrap();
31/// ```
32#[derive(Clone, Debug)]
33pub struct Quantity {
34    value: f64,
35    unit: Unit,
36}
37
38impl Quantity {
39    /// Create a new quantity with a value and unit.
40    pub fn new(value: f64, unit: Unit) -> Self {
41        Quantity { value, unit }
42    }
43
44    /// Get the numeric value.
45    pub fn value(&self) -> f64 {
46        self.value
47    }
48
49    /// Get the unit.
50    pub fn unit(&self) -> &Unit {
51        &self.unit
52    }
53
54    /// Check if this quantity is dimensionless.
55    pub fn is_dimensionless(&self) -> bool {
56        self.unit.is_dimensionless()
57    }
58
59    /// Get the value as a dimensionless scalar.
60    ///
61    /// Returns `Err` if the quantity is not dimensionless.
62    pub fn dimensionless_value(&self) -> UnitResult<f64> {
63        if !self.is_dimensionless() {
64            return Err(UnitError::NotDimensionless);
65        }
66        Ok(self.value * self.unit.scale())
67    }
68
69    /// Convert to another unit.
70    ///
71    /// Returns `Err` if the units have incompatible dimensions.
72    pub fn to(&self, target: impl Into<Unit>) -> UnitResult<Quantity> {
73        let target = target.into();
74        if self.unit.dimension() != target.dimension() {
75            return Err(UnitError::DimensionMismatch {
76                from: self.unit.to_string(),
77                to: target.to_string(),
78            });
79        }
80        let si_value = self.unit.to_si(self.value);
81        Ok(Quantity::new(target.from_si(si_value), target))
82    }
83
84    /// Get the value in a target unit.
85    ///
86    /// Shorthand for `.to(target)?.value()`.
87    pub fn to_value(&self, target: impl Into<Unit>) -> UnitResult<f64> {
88        Ok(self.to(target)?.value())
89    }
90
91    /// Decompose to SI base units.
92    pub fn decompose(&self) -> Quantity {
93        Quantity::new(
94            self.unit.to_si(self.value),
95            Unit::Composite(crate::unit::composite::CompositeUnit::new(
96                1.0,
97                self.dimension_components(),
98            )),
99        )
100    }
101
102    /// Get the dimension components for the decomposed unit.
103    fn dimension_components(&self) -> Vec<crate::unit::composite::UnitComponent> {
104        use crate::dimension::{Dimension, Rational16};
105        use crate::unit::composite::UnitComponent;
106
107        let dim = self.unit.dimension();
108        let mut components = Vec::new();
109
110        let add_if_nonzero =
111            |comps: &mut Vec<UnitComponent>, symbol: &str, base_dim: Dimension, exp: Rational16| {
112                if !exp.is_zero() {
113                    comps.push(UnitComponent::new(symbol, base_dim, 1.0, exp));
114                }
115            };
116
117        add_if_nonzero(&mut components, "m", Dimension::LENGTH, dim.length);
118        add_if_nonzero(&mut components, "s", Dimension::TIME, dim.time);
119        add_if_nonzero(&mut components, "kg", Dimension::MASS, dim.mass);
120        add_if_nonzero(&mut components, "A", Dimension::CURRENT, dim.current);
121        add_if_nonzero(
122            &mut components,
123            "K",
124            Dimension::TEMPERATURE,
125            dim.temperature,
126        );
127        add_if_nonzero(&mut components, "rad", Dimension::ANGLE, dim.angle);
128        add_if_nonzero(
129            &mut components,
130            "sr",
131            Dimension::SOLID_ANGLE,
132            dim.solid_angle,
133        );
134        add_if_nonzero(
135            &mut components,
136            "cd",
137            Dimension::LUMINOUS_INTENSITY,
138            dim.luminous_intensity,
139        );
140        add_if_nonzero(&mut components, "mag", Dimension::MAGNITUDE, dim.magnitude);
141        add_if_nonzero(&mut components, "mol", Dimension::AMOUNT, dim.amount);
142        add_if_nonzero(&mut components, "ph", Dimension::PHOTON, dim.photon);
143
144        components
145    }
146
147    /// Raise this quantity to a power.
148    ///
149    /// Accepts any type convertible to `Rational16`, including `Rational16` itself.
150    /// When passing an `i32`, the value must fit in `i16` range.
151    ///
152    /// Uses `powi` for integer exponents and `powf` for fractional exponents.
153    pub fn pow(&self, exp: impl Into<Rational16>) -> Quantity {
154        let exp = exp.into();
155        let value = if exp.denom == 1 {
156            self.value.powi(exp.numer as i32)
157        } else {
158            self.value.powf(exp.to_f64())
159        };
160        Quantity::new(value, self.unit.pow(exp))
161    }
162
163    /// Take the square root of this quantity.
164    pub fn sqrt(&self) -> Quantity {
165        Quantity::new(self.value.sqrt(), self.unit.sqrt())
166    }
167
168    /// Get the absolute value of this quantity.
169    pub fn abs(&self) -> Quantity {
170        Quantity::new(self.value.abs(), self.unit.clone())
171    }
172
173    // =========================================================================
174    // Logarithmic Unit Helpers
175    // =========================================================================
176
177    /// Check if this quantity has a logarithmic unit (magnitude dimension).
178    ///
179    /// Logarithmic units include magnitudes, decibels, and dex.
180    ///
181    /// # Example
182    ///
183    /// ```no_run
184    /// # #[cfg(feature = "logarithmic")]
185    /// # fn main() {
186    /// use iridium_units::prelude::*;
187    /// use iridium_units::systems::logarithmic::MAG;
188    ///
189    /// let mag = 5.0 * MAG;
190    /// assert!(mag.is_logarithmic());
191    ///
192    /// let length = 10.0 * M;
193    /// assert!(!length.is_logarithmic());
194    /// # }
195    /// # #[cfg(not(feature = "logarithmic"))]
196    /// # fn main() {}
197    /// ```
198    pub fn is_logarithmic(&self) -> bool {
199        self.unit.dimension() == crate::dimension::Dimension::MAGNITUDE
200    }
201
202    /// Convert a magnitude quantity to a flux ratio.
203    ///
204    /// Uses the Pogson formula: F/F₀ = 10^(-0.4 * m)
205    ///
206    /// Returns `Err` if this quantity does not have magnitude dimension.
207    ///
208    /// # Example
209    ///
210    /// ```no_run
211    /// # #[cfg(feature = "logarithmic")]
212    /// # fn main() {
213    /// use iridium_units::prelude::*;
214    /// use iridium_units::systems::logarithmic::MAG;
215    ///
216    /// let star = 5.0 * MAG;  // 5th magnitude
217    /// let flux = star.mag_to_flux_ratio().unwrap();
218    /// assert!((flux - 0.01).abs() < 1e-10);  // 1/100 of reference flux
219    /// # }
220    /// # #[cfg(not(feature = "logarithmic"))]
221    /// # fn main() {}
222    /// ```
223    pub fn mag_to_flux_ratio(&self) -> UnitResult<f64> {
224        if !self.is_logarithmic() {
225            return Err(UnitError::LogarithmicError(
226                "quantity is not a magnitude".to_string(),
227            ));
228        }
229        // Convert to standard magnitude (scale = 1)
230        let mag = self.value * self.unit.scale();
231        Ok(10.0_f64.powf(-0.4 * mag))
232    }
233
234    /// Convert a decibel quantity to a power ratio.
235    ///
236    /// Uses the formula: P/P₀ = 10^(dB/10)
237    ///
238    /// Returns `Err` if this quantity does not have magnitude dimension.
239    ///
240    /// # Example
241    ///
242    /// ```no_run
243    /// # #[cfg(feature = "logarithmic")]
244    /// # fn main() {
245    /// use iridium_units::prelude::*;
246    /// use iridium_units::systems::logarithmic::DB;
247    ///
248    /// let signal = 10.0 * DB;  // 10 dB
249    /// let power = signal.db_to_power_ratio().unwrap();
250    /// assert!((power - 10.0).abs() < 1e-10);  // 10x power
251    /// # }
252    /// # #[cfg(not(feature = "logarithmic"))]
253    /// # fn main() {}
254    /// ```
255    pub fn db_to_power_ratio(&self) -> UnitResult<f64> {
256        if !self.is_logarithmic() {
257            return Err(UnitError::LogarithmicError(
258                "quantity is not in decibels".to_string(),
259            ));
260        }
261        let db = self.value * self.unit.scale();
262        Ok(10.0_f64.powf(db / 10.0))
263    }
264
265    /// Convert a dex quantity to a linear ratio.
266    ///
267    /// Uses the formula: x/x₀ = 10^dex
268    ///
269    /// Returns `Err` if this quantity does not have magnitude dimension.
270    ///
271    /// # Example
272    ///
273    /// ```no_run
274    /// # #[cfg(feature = "logarithmic")]
275    /// # fn main() {
276    /// use iridium_units::prelude::*;
277    /// use iridium_units::systems::logarithmic::DEX;
278    ///
279    /// let order = 2.0 * DEX;  // 2 orders of magnitude
280    /// let ratio = order.dex_to_ratio().unwrap();
281    /// assert!((ratio - 100.0).abs() < 1e-10);  // factor of 100
282    /// # }
283    /// # #[cfg(not(feature = "logarithmic"))]
284    /// # fn main() {}
285    /// ```
286    pub fn dex_to_ratio(&self) -> UnitResult<f64> {
287        if !self.is_logarithmic() {
288            return Err(UnitError::LogarithmicError(
289                "quantity is not in dex".to_string(),
290            ));
291        }
292        let dex = self.value * self.unit.scale();
293        Ok(10.0_f64.powf(dex))
294    }
295}
296
297// =============================================================================
298// Checked Arithmetic
299// =============================================================================
300
301impl Quantity {
302    /// Add two quantities, returning an error if their dimensions don't match.
303    ///
304    /// This is the fallible version of the `+` operator.
305    ///
306    /// ```
307    /// use iridium_units::prelude::*;
308    ///
309    /// let a = 1.0 * KM;
310    /// let b = 500.0 * M;
311    /// let c = a.checked_add(&b).unwrap();
312    /// assert!((c.value() - 1.5).abs() < 1e-10);
313    /// ```
314    pub fn checked_add(&self, rhs: &Quantity) -> UnitResult<Quantity> {
315        if self.unit.dimension() != rhs.unit.dimension() {
316            return Err(UnitError::IncompatibleDimensions {
317                lhs: self.unit.to_string(),
318                rhs: rhs.unit.to_string(),
319            });
320        }
321        let rhs_converted = self.unit.from_si(rhs.unit.to_si(rhs.value));
322        Ok(Quantity::new(self.value + rhs_converted, self.unit.clone()))
323    }
324
325    /// Subtract two quantities, returning an error if their dimensions don't match.
326    ///
327    /// This is the fallible version of the `-` operator.
328    ///
329    /// ```
330    /// use iridium_units::prelude::*;
331    ///
332    /// let a = 1.0 * KM;
333    /// let b = 500.0 * M;
334    /// let c = a.checked_sub(&b).unwrap();
335    /// assert!((c.value() - 0.5).abs() < 1e-10);
336    /// ```
337    pub fn checked_sub(&self, rhs: &Quantity) -> UnitResult<Quantity> {
338        if self.unit.dimension() != rhs.unit.dimension() {
339            return Err(UnitError::IncompatibleDimensions {
340                lhs: self.unit.to_string(),
341                rhs: rhs.unit.to_string(),
342            });
343        }
344        let rhs_converted = self.unit.from_si(rhs.unit.to_si(rhs.value));
345        Ok(Quantity::new(self.value - rhs_converted, self.unit.clone()))
346    }
347}
348
349// =============================================================================
350// Batch Conversion API
351// =============================================================================
352
353/// Convert a slice of values from one unit to another.
354///
355/// This is more efficient than creating individual `Quantity` objects when
356/// processing large datasets, as it computes the conversion factor once
357/// and applies it to all values.
358///
359/// Accepts any `impl Into<Unit>`, including `BaseUnit`, `&BaseUnit`, `Unit`,
360/// and `&Unit`. When passing a borrowed unit, it is cloned once (amortized
361/// over the batch). Pass an owned value to avoid the clone.
362///
363/// Note: This uses a single multiplicative factor, so it does not support
364/// offset units like Celsius or Fahrenheit. Use [`Quantity::to()`] for those.
365///
366/// # Example
367///
368/// ```
369/// use iridium_units::prelude::*;
370/// use iridium_units::quantity::batch_convert;
371///
372/// let distances_km = vec![1.0, 2.0, 3.0, 100.0, 42.195];
373/// let distances_m = batch_convert(&distances_km, &KM, &M).unwrap();
374/// assert!((distances_m[0] - 1000.0).abs() < 1e-10);
375/// assert!((distances_m[4] - 42195.0).abs() < 1e-10);
376/// ```
377pub fn batch_convert(
378    values: &[f64],
379    from: impl Into<Unit>,
380    to: impl Into<Unit>,
381) -> UnitResult<Vec<f64>> {
382    let from = from.into();
383    let to = to.into();
384    let factor = from.conversion_factor(&to)?;
385    Ok(values.iter().map(|v| v * factor).collect())
386}
387
388/// Convert a slice of values from one unit to another, writing into a pre-allocated buffer.
389///
390/// This variant avoids allocation when the output buffer already exists.
391/// Returns `Err` if the units have incompatible dimensions, if either unit
392/// has an additive offset, or if the output slice length doesn't match the input.
393///
394/// # Example
395///
396/// ```
397/// use iridium_units::prelude::*;
398/// use iridium_units::quantity::batch_convert_into;
399///
400/// let distances_km = [1.0, 2.0, 3.0];
401/// let mut distances_m = [0.0; 3];
402/// batch_convert_into(&distances_km, &KM, &M, &mut distances_m).unwrap();
403/// assert!((distances_m[0] - 1000.0).abs() < 1e-10);
404/// ```
405pub fn batch_convert_into(
406    values: &[f64],
407    from: impl Into<Unit>,
408    to: impl Into<Unit>,
409    out: &mut [f64],
410) -> UnitResult<()> {
411    if values.len() != out.len() {
412        return Err(UnitError::BatchError(format!(
413            "input length {} doesn't match output length {}",
414            values.len(),
415            out.len()
416        )));
417    }
418    let from = from.into();
419    let to = to.into();
420    let factor = from.conversion_factor(&to)?;
421    for (i, v) in values.iter().enumerate() {
422        out[i] = v * factor;
423    }
424    Ok(())
425}
426
427/// Get the conversion factor between two units for manual batch operations.
428///
429/// This is useful when you need to apply the conversion factor yourself,
430/// such as in SIMD operations or when working with external array libraries.
431///
432/// Does not support offset units (Celsius, Fahrenheit). Use [`Quantity::to()`]
433/// for those.
434///
435/// # Example
436///
437/// ```
438/// use iridium_units::prelude::*;
439/// use iridium_units::quantity::conversion_factor;
440///
441/// let factor = conversion_factor(&KM, &M).unwrap();
442/// assert!((factor - 1000.0).abs() < 1e-10);
443///
444/// // Apply manually to any data structure
445/// let value_km = 5.0;
446/// let value_m = value_km * factor;
447/// ```
448pub fn conversion_factor(from: impl Into<Unit>, to: impl Into<Unit>) -> UnitResult<f64> {
449    let from = from.into();
450    let to = to.into();
451    from.conversion_factor(&to)
452}
453
454impl fmt::Display for Quantity {
455    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456        let unit_str = self.unit.to_string();
457        if unit_str.is_empty() || unit_str == "dimensionless" {
458            write!(f, "{}", self.value)
459        } else {
460            write!(f, "{} {}", self.value, unit_str)
461        }
462    }
463}
464
465impl PartialEq for Quantity {
466    fn eq(&self, other: &Self) -> bool {
467        // Two quantities are equal if they have the same dimension and
468        // the same value when converted to the same unit
469        if self.unit.dimension() != other.unit.dimension() {
470            return false;
471        }
472        let self_si = self.unit.to_si(self.value);
473        let other_si = other.unit.to_si(other.value);
474        (self_si - other_si).abs() < 1e-15 * self_si.abs().max(other_si.abs()).max(1e-15)
475    }
476}
477
478// Quantity + Quantity
479//
480// Panics on dimension mismatch. Use `checked_add` for a fallible version.
481impl Add for Quantity {
482    type Output = Quantity;
483
484    fn add(self, rhs: Quantity) -> Quantity {
485        if self.unit.dimension() != rhs.unit.dimension() {
486            panic!(
487                "{}",
488                UnitError::IncompatibleDimensions {
489                    lhs: self.unit.to_string(),
490                    rhs: rhs.unit.to_string(),
491                }
492            );
493        }
494        let rhs_converted = self.unit.from_si(rhs.unit.to_si(rhs.value));
495        Quantity::new(self.value + rhs_converted, self.unit)
496    }
497}
498
499impl Add for &Quantity {
500    type Output = Quantity;
501
502    fn add(self, rhs: &Quantity) -> Quantity {
503        self.checked_add(rhs).unwrap_or_else(|e| panic!("{}", e))
504    }
505}
506
507// Quantity - Quantity
508//
509// Panics on dimension mismatch. Use `checked_sub` for a fallible version.
510impl Sub for Quantity {
511    type Output = Quantity;
512
513    fn sub(self, rhs: Quantity) -> Quantity {
514        if self.unit.dimension() != rhs.unit.dimension() {
515            panic!(
516                "{}",
517                UnitError::IncompatibleDimensions {
518                    lhs: self.unit.to_string(),
519                    rhs: rhs.unit.to_string(),
520                }
521            );
522        }
523        let rhs_converted = self.unit.from_si(rhs.unit.to_si(rhs.value));
524        Quantity::new(self.value - rhs_converted, self.unit)
525    }
526}
527
528impl Sub for &Quantity {
529    type Output = Quantity;
530
531    fn sub(self, rhs: &Quantity) -> Quantity {
532        self.checked_sub(rhs).unwrap_or_else(|e| panic!("{}", e))
533    }
534}
535
536// Quantity * Quantity
537impl Mul for Quantity {
538    type Output = Quantity;
539
540    fn mul(self, rhs: Quantity) -> Quantity {
541        Quantity::new(self.value * rhs.value, self.unit * rhs.unit)
542    }
543}
544
545impl Mul for &Quantity {
546    type Output = Quantity;
547
548    fn mul(self, rhs: &Quantity) -> Quantity {
549        Quantity::new(self.value * rhs.value, &self.unit * &rhs.unit)
550    }
551}
552
553impl Mul<&Quantity> for Quantity {
554    type Output = Quantity;
555
556    fn mul(self, rhs: &Quantity) -> Quantity {
557        Quantity::new(self.value * rhs.value, self.unit * &rhs.unit)
558    }
559}
560
561impl Mul<Quantity> for &Quantity {
562    type Output = Quantity;
563
564    fn mul(self, rhs: Quantity) -> Quantity {
565        Quantity::new(self.value * rhs.value, &self.unit * rhs.unit)
566    }
567}
568
569// Quantity / Quantity
570impl Div for Quantity {
571    type Output = Quantity;
572
573    fn div(self, rhs: Quantity) -> Quantity {
574        Quantity::new(self.value / rhs.value, self.unit / rhs.unit)
575    }
576}
577
578impl Div for &Quantity {
579    type Output = Quantity;
580
581    fn div(self, rhs: &Quantity) -> Quantity {
582        Quantity::new(self.value / rhs.value, &self.unit / &rhs.unit)
583    }
584}
585
586impl Div<&Quantity> for Quantity {
587    type Output = Quantity;
588
589    fn div(self, rhs: &Quantity) -> Quantity {
590        Quantity::new(self.value / rhs.value, self.unit / &rhs.unit)
591    }
592}
593
594impl Div<Quantity> for &Quantity {
595    type Output = Quantity;
596
597    fn div(self, rhs: Quantity) -> Quantity {
598        Quantity::new(self.value / rhs.value, &self.unit / rhs.unit)
599    }
600}
601
602// Quantity * f64
603impl Mul<f64> for Quantity {
604    type Output = Quantity;
605
606    fn mul(self, rhs: f64) -> Quantity {
607        Quantity::new(self.value * rhs, self.unit)
608    }
609}
610
611impl Mul<f64> for &Quantity {
612    type Output = Quantity;
613
614    fn mul(self, rhs: f64) -> Quantity {
615        Quantity::new(self.value * rhs, self.unit.clone())
616    }
617}
618
619// f64 * Quantity
620impl Mul<Quantity> for f64 {
621    type Output = Quantity;
622
623    fn mul(self, rhs: Quantity) -> Quantity {
624        Quantity::new(self * rhs.value, rhs.unit)
625    }
626}
627
628impl Mul<&Quantity> for f64 {
629    type Output = Quantity;
630
631    fn mul(self, rhs: &Quantity) -> Quantity {
632        Quantity::new(self * rhs.value, rhs.unit.clone())
633    }
634}
635
636// Quantity / f64
637impl Div<f64> for Quantity {
638    type Output = Quantity;
639
640    fn div(self, rhs: f64) -> Quantity {
641        Quantity::new(self.value / rhs, self.unit)
642    }
643}
644
645impl Div<f64> for &Quantity {
646    type Output = Quantity;
647
648    fn div(self, rhs: f64) -> Quantity {
649        Quantity::new(self.value / rhs, self.unit.clone())
650    }
651}
652
653// f64 / Quantity
654impl Div<Quantity> for f64 {
655    type Output = Quantity;
656
657    fn div(self, rhs: Quantity) -> Quantity {
658        Quantity::new(self / rhs.value, rhs.unit.inv())
659    }
660}
661
662impl Div<&Quantity> for f64 {
663    type Output = Quantity;
664
665    fn div(self, rhs: &Quantity) -> Quantity {
666        Quantity::new(self / rhs.value, rhs.unit.inv())
667    }
668}
669
670// -Quantity
671impl Neg for Quantity {
672    type Output = Quantity;
673
674    fn neg(self) -> Quantity {
675        Quantity::new(-self.value, self.unit)
676    }
677}
678
679impl Neg for &Quantity {
680    type Output = Quantity;
681
682    fn neg(self) -> Quantity {
683        Quantity::new(-self.value, self.unit.clone())
684    }
685}
686
687// f64 * Unit -> Quantity (the main way to create quantities)
688impl Mul<Unit> for f64 {
689    type Output = Quantity;
690
691    fn mul(self, unit: Unit) -> Quantity {
692        Quantity::new(self, unit)
693    }
694}
695
696impl Mul<&Unit> for f64 {
697    type Output = Quantity;
698
699    fn mul(self, unit: &Unit) -> Quantity {
700        Quantity::new(self, unit.clone())
701    }
702}
703
704// f64 * BaseUnit → Quantity
705impl Mul<BaseUnit> for f64 {
706    type Output = Quantity;
707
708    fn mul(self, unit: BaseUnit) -> Quantity {
709        Quantity::new(self, Unit::from(unit))
710    }
711}
712
713// BaseUnit * f64 → Quantity
714impl Mul<f64> for BaseUnit {
715    type Output = Quantity;
716
717    fn mul(self, value: f64) -> Quantity {
718        Quantity::new(value, Unit::from(self))
719    }
720}
721
722// Quantity * BaseUnit
723impl Mul<BaseUnit> for Quantity {
724    type Output = Quantity;
725
726    fn mul(self, unit: BaseUnit) -> Quantity {
727        self * Unit::from(unit)
728    }
729}
730
731impl Mul<BaseUnit> for &Quantity {
732    type Output = Quantity;
733
734    fn mul(self, unit: BaseUnit) -> Quantity {
735        Quantity::new(self.value, &self.unit * unit)
736    }
737}
738
739// Quantity / BaseUnit
740impl Div<BaseUnit> for Quantity {
741    type Output = Quantity;
742
743    fn div(self, unit: BaseUnit) -> Quantity {
744        self / Unit::from(unit)
745    }
746}
747
748impl Div<BaseUnit> for &Quantity {
749    type Output = Quantity;
750
751    fn div(self, unit: BaseUnit) -> Quantity {
752        Quantity::new(self.value, &self.unit / unit)
753    }
754}
755
756// Quantity * Unit
757impl Mul<Unit> for Quantity {
758    type Output = Quantity;
759
760    fn mul(self, unit: Unit) -> Quantity {
761        Quantity::new(self.value, self.unit * unit)
762    }
763}
764
765impl Mul<&Unit> for Quantity {
766    type Output = Quantity;
767
768    fn mul(self, unit: &Unit) -> Quantity {
769        Quantity::new(self.value, self.unit * unit)
770    }
771}
772
773impl Mul<Unit> for &Quantity {
774    type Output = Quantity;
775
776    fn mul(self, unit: Unit) -> Quantity {
777        Quantity::new(self.value, &self.unit * unit)
778    }
779}
780
781impl Mul<&Unit> for &Quantity {
782    type Output = Quantity;
783
784    fn mul(self, unit: &Unit) -> Quantity {
785        Quantity::new(self.value, &self.unit * unit)
786    }
787}
788
789// Quantity / Unit
790impl Div<Unit> for Quantity {
791    type Output = Quantity;
792
793    fn div(self, unit: Unit) -> Quantity {
794        Quantity::new(self.value, self.unit / unit)
795    }
796}
797
798impl Div<&Unit> for Quantity {
799    type Output = Quantity;
800
801    fn div(self, unit: &Unit) -> Quantity {
802        Quantity::new(self.value, self.unit / unit)
803    }
804}
805
806impl Div<Unit> for &Quantity {
807    type Output = Quantity;
808
809    fn div(self, unit: Unit) -> Quantity {
810        Quantity::new(self.value, &self.unit / unit)
811    }
812}
813
814impl Div<&Unit> for &Quantity {
815    type Output = Quantity;
816
817    fn div(self, unit: &Unit) -> Quantity {
818        Quantity::new(self.value, &self.unit / unit)
819    }
820}
821
822#[cfg(test)]
823mod tests {
824    use super::*;
825    use crate::dimension::Dimension;
826    use crate::unit::base::BaseUnit;
827
828    fn meter() -> Unit {
829        Unit::Base(BaseUnit::new("meter", "m", &[], Dimension::LENGTH, 1.0))
830    }
831
832    fn kilometer() -> Unit {
833        Unit::Base(BaseUnit::new(
834            "kilometer",
835            "km",
836            &[],
837            Dimension::LENGTH,
838            1000.0,
839        ))
840    }
841
842    fn second() -> Unit {
843        Unit::Base(BaseUnit::new("second", "s", &[], Dimension::TIME, 1.0))
844    }
845
846    #[test]
847    fn test_quantity_creation() {
848        let q = 5.0 * meter();
849        assert_eq!(q.value(), 5.0);
850    }
851
852    #[test]
853    fn test_quantity_conversion() {
854        let q = 1.0 * kilometer();
855        let q_m = q.to(&meter()).unwrap();
856        assert!((q_m.value() - 1000.0).abs() < 1e-10);
857    }
858
859    #[test]
860    fn test_quantity_addition() {
861        let a = 1.0 * kilometer();
862        let b = 500.0 * meter();
863        let c = a + b;
864        assert!((c.value() - 1.5).abs() < 1e-10); // 1.5 km
865    }
866
867    #[test]
868    fn test_quantity_subtraction() {
869        let a = 1.0 * kilometer();
870        let b = 500.0 * meter();
871        let c = a - b;
872        assert!((c.value() - 0.5).abs() < 1e-10); // 0.5 km
873    }
874
875    #[test]
876    fn test_quantity_multiplication() {
877        let a = 10.0 * meter();
878        let b = 5.0 * second();
879        let c = a * b;
880        assert!((c.value() - 50.0).abs() < 1e-10);
881    }
882
883    #[test]
884    fn test_quantity_division() {
885        let dist = 100.0 * meter();
886        let time = 10.0 * second();
887        let speed = dist / time;
888        assert!((speed.value() - 10.0).abs() < 1e-10);
889    }
890
891    #[test]
892    #[should_panic(expected = "cannot add/subtract quantities with different dimensions")]
893    fn test_incompatible_addition() {
894        let a = 1.0 * meter();
895        let b = 1.0 * second();
896        let _ = a + b;
897    }
898
899    #[test]
900    fn test_checked_add_incompatible() {
901        let a = 1.0 * meter();
902        let b = 1.0 * second();
903        let result = a.checked_add(&b);
904        assert!(matches!(
905            result,
906            Err(UnitError::IncompatibleDimensions { .. })
907        ));
908    }
909
910    #[test]
911    fn test_checked_sub_incompatible() {
912        let a = 1.0 * meter();
913        let b = 1.0 * second();
914        let result = a.checked_sub(&b);
915        assert!(matches!(
916            result,
917            Err(UnitError::IncompatibleDimensions { .. })
918        ));
919    }
920
921    #[test]
922    fn test_quantity_display() {
923        let q = 5.5 * meter();
924        assert_eq!(format!("{}", q), "5.5 m");
925    }
926
927    #[test]
928    fn test_ref_addition_no_clone() {
929        // Test that reference addition works correctly
930        let a = 1.0 * kilometer();
931        let b = 500.0 * meter();
932        let c = &a + &b;
933        assert!((c.value() - 1.5).abs() < 1e-10); // 1.5 km
934                                                  // Original values should still be accessible
935        assert!((a.value() - 1.0).abs() < 1e-10);
936        assert!((b.value() - 500.0).abs() < 1e-10);
937    }
938
939    #[test]
940    fn test_ref_subtraction_no_clone() {
941        // Test that reference subtraction works correctly
942        let a = 1.0 * kilometer();
943        let b = 500.0 * meter();
944        let c = &a - &b;
945        assert!((c.value() - 0.5).abs() < 1e-10); // 0.5 km
946    }
947
948    #[test]
949    fn test_batch_convert() {
950        use super::batch_convert;
951        let values = vec![1.0, 2.0, 3.0, 100.0];
952        let converted = batch_convert(&values, &kilometer(), &meter()).unwrap();
953        assert_eq!(converted.len(), 4);
954        assert!((converted[0] - 1000.0).abs() < 1e-10);
955        assert!((converted[1] - 2000.0).abs() < 1e-10);
956        assert!((converted[2] - 3000.0).abs() < 1e-10);
957        assert!((converted[3] - 100000.0).abs() < 1e-10);
958    }
959
960    #[test]
961    fn test_batch_convert_into() {
962        use super::batch_convert_into;
963        let values = [1.0, 2.0, 3.0];
964        let mut out = [0.0; 3];
965        batch_convert_into(&values, &kilometer(), &meter(), &mut out).unwrap();
966        assert!((out[0] - 1000.0).abs() < 1e-10);
967        assert!((out[1] - 2000.0).abs() < 1e-10);
968        assert!((out[2] - 3000.0).abs() < 1e-10);
969    }
970
971    #[test]
972    fn test_batch_convert_into_length_mismatch() {
973        use super::batch_convert_into;
974        let values = [1.0, 2.0, 3.0];
975        let mut out = [0.0; 2]; // Wrong length
976        let result = batch_convert_into(&values, &kilometer(), &meter(), &mut out);
977        assert!(result.is_err());
978    }
979
980    #[test]
981    fn test_batch_convert_incompatible_units() {
982        use super::batch_convert;
983        let values = vec![1.0, 2.0];
984        let result = batch_convert(&values, &meter(), &second());
985        assert!(result.is_err());
986    }
987
988    #[test]
989    fn test_conversion_factor() {
990        use super::conversion_factor;
991        let factor = conversion_factor(&kilometer(), &meter()).unwrap();
992        assert!((factor - 1000.0).abs() < 1e-10);
993    }
994
995    #[test]
996    fn test_pow_integer() {
997        let m = 3.0 * meter();
998        let m2 = m.pow(2);
999        assert!((m2.value() - 9.0).abs() < 1e-10);
1000        assert_eq!(
1001            m2.unit().dimension(),
1002            Dimension {
1003                length: Rational16::new(2, 1),
1004                ..Dimension::DIMENSIONLESS
1005            }
1006        );
1007    }
1008
1009    #[test]
1010    fn test_pow_fractional() {
1011        // sqrt(4 m²) = 2 m
1012        let area = 4.0 * meter().pow(Rational16::new(2, 1));
1013        let root = area.pow(Rational16::new(1, 2));
1014        assert!((root.value() - 2.0).abs() < 1e-10);
1015        assert_eq!(root.unit().dimension(), Dimension::LENGTH);
1016    }
1017}