Skip to main content

iridium_units/unit/
mod.rs

1//! Unit types and operations.
2//!
3//! This module provides the [`Unit`] type, which represents a physical unit
4//! like meter, kilogram, or m/s. Most users interact with units through
5//! the predefined constants in [`crate::systems`] (e.g., `M`, `KG`, `HZ`).
6//!
7//! # Creating quantities
8//!
9//! ```
10//! use iridium_units::prelude::*;
11//!
12//! let distance = 100.0 * KM;
13//! let time = 2.0 * H;
14//! let speed = &distance / &time;
15//!
16//! // Convert to a different unit
17//! let speed_ms = speed.to(M / S).unwrap();
18//! assert!((speed_ms.value() - 13.8888).abs() < 0.001);
19//! ```
20//!
21//! # Composite units from arithmetic
22//!
23//! ```
24//! use iridium_units::prelude::*;
25//!
26//! let velocity_unit = M / S;           // m/s
27//! let accel_unit = M / S.pow(2);       // m/s²
28//! let force_unit = KG * M / S.pow(2);  // kg·m/s² (newton)
29//! ```
30
31pub mod base;
32pub mod composite;
33
34use crate::dimension::{Dimension, Rational16};
35use crate::error::{UnitError, UnitResult};
36use base::BaseUnit;
37use composite::CompositeUnit;
38use std::fmt;
39use std::ops::{Div, Mul};
40
41/// A physical unit.
42///
43/// Units can be base units (like meter), named derived units (like newton),
44/// composite units (like m/s), or dimensionless.
45///
46/// # Examples
47///
48/// ```
49/// use iridium_units::prelude::*;
50///
51/// // Base units are Copy — use them directly
52/// let mass = 10.0 * KG;
53///
54/// // Arithmetic creates composite units
55/// let velocity_unit = KM / H;
56///
57/// // Check dimensions
58/// assert_eq!((M / S).dimension(), (KM / H).dimension());
59///
60/// // Convert between compatible units
61/// let speed = 100.0 * &(KM / H);
62/// let in_ms = speed.to(M / S).unwrap();
63/// ```
64#[derive(Clone, Debug, PartialEq)]
65pub enum Unit {
66    /// A base irreducible unit (meter, second, kilogram, etc.)
67    Base(BaseUnit),
68
69    /// A composite unit from arithmetic operations (m/s, kg·m/s², etc.)
70    Composite(CompositeUnit),
71
72    /// Dimensionless with a scale factor.
73    Dimensionless {
74        /// Multiplicative scale factor (1.0 for pure dimensionless).
75        scale: f64,
76    },
77}
78
79impl Unit {
80    /// Create a dimensionless unit with scale 1.
81    pub fn dimensionless() -> Self {
82        Unit::Dimensionless { scale: 1.0 }
83    }
84
85    /// Create a dimensionless unit with a scale factor.
86    pub fn dimensionless_scaled(scale: f64) -> Self {
87        Unit::Dimensionless { scale }
88    }
89
90    /// Create a unit from a base unit.
91    pub fn from_base(base: &BaseUnit) -> Self {
92        Unit::Base(*base)
93    }
94
95    /// Get the dimension of this unit.
96    pub fn dimension(&self) -> Dimension {
97        match self {
98            Unit::Base(b) => b.dimension,
99            Unit::Composite(c) => c.dimension(),
100            Unit::Dimensionless { .. } => Dimension::DIMENSIONLESS,
101        }
102    }
103
104    /// Get the scale factor relative to SI base units.
105    pub fn scale(&self) -> f64 {
106        match self {
107            Unit::Base(b) => b.scale,
108            Unit::Composite(c) => c.total_scale(),
109            Unit::Dimensionless { scale } => *scale,
110        }
111    }
112
113    /// Check if this unit is dimensionless.
114    pub fn is_dimensionless(&self) -> bool {
115        self.dimension().is_dimensionless()
116    }
117
118    /// Get the additive offset relative to the SI base unit.
119    ///
120    /// Most units have offset 0.0. Offset units like Celsius (273.15)
121    /// and Fahrenheit (459.67) use this for affine conversions.
122    /// The formula is: `SI_value = (value + offset) * scale`.
123    ///
124    /// Offsets are only defined for [`Unit::Base`]. Composite units are
125    /// treated as interval units, so composing an offset base unit does
126    /// **not** preserve its additive offset.
127    pub fn offset(&self) -> f64 {
128        match self {
129            Unit::Base(b) => b.offset,
130            Unit::Composite(_) | Unit::Dimensionless { .. } => 0.0,
131        }
132    }
133
134    /// Check if this unit has an additive offset (e.g., Celsius, Fahrenheit).
135    ///
136    /// Only true for [`Unit::Base`] values with a non-zero offset.
137    pub fn has_offset(&self) -> bool {
138        self.offset() != 0.0
139    }
140
141    /// Convert a value in this unit to SI base units.
142    ///
143    /// For pure scale units: `value * scale`
144    /// For offset base units: `(value + offset) * scale`
145    ///
146    /// Affine offsets are only applied for [`Unit::Base`]. Composite
147    /// units are converted using scale alone (interval semantics).
148    pub fn to_si(&self, value: f64) -> f64 {
149        (value + self.offset()) * self.scale()
150    }
151
152    /// Convert a value from SI base units to this unit.
153    ///
154    /// For simple units: `si_value / scale`
155    /// For offset units: `si_value / scale - offset`
156    pub fn from_si(&self, si_value: f64) -> f64 {
157        si_value / self.scale() - self.offset()
158    }
159
160    /// Get the conversion factor to convert from this unit to another.
161    ///
162    /// This returns a single multiplicative factor, which only works for
163    /// units without additive offsets. For offset units like Celsius or
164    /// Fahrenheit, use [`Quantity::to`](crate::Quantity::to) instead.
165    ///
166    /// Returns `Err` if the units have incompatible dimensions or if
167    /// either unit has an additive offset.
168    pub fn conversion_factor(&self, to: &Unit) -> UnitResult<f64> {
169        if self.dimension() != to.dimension() {
170            return Err(UnitError::DimensionMismatch {
171                from: self.to_string(),
172                to: to.to_string(),
173            });
174        }
175        // Identity conversion is always valid, even for offset units
176        if self == to {
177            return Ok(1.0);
178        }
179        if self.has_offset() || to.has_offset() {
180            return Err(UnitError::OffsetConversion {
181                from: self.to_string(),
182                to: to.to_string(),
183            });
184        }
185        Ok(self.scale() / to.scale())
186    }
187
188    /// Convert this unit to a composite representation.
189    pub fn to_composite(&self) -> CompositeUnit {
190        match self {
191            Unit::Base(b) => CompositeUnit::from_base(b.symbol, b.dimension, b.scale),
192            Unit::Composite(c) => c.clone(),
193            Unit::Dimensionless { scale } => CompositeUnit::dimensionless(*scale),
194        }
195    }
196
197    /// Raise this unit to a power.
198    pub fn pow(&self, exp: impl Into<Rational16>) -> Unit {
199        let exp = exp.into();
200        if exp.is_zero() {
201            return Unit::dimensionless();
202        }
203        if exp == Rational16::ONE {
204            return self.clone();
205        }
206        Unit::Composite(self.to_composite().pow(exp))
207    }
208
209    /// Take the square root of this unit.
210    pub fn sqrt(&self) -> Unit {
211        self.pow(Rational16::new(1, 2))
212    }
213
214    /// Invert this unit (raise to power -1).
215    pub fn inv(&self) -> Unit {
216        self.pow(Rational16::new(-1, 1))
217    }
218
219    /// Get the symbol/string representation for this unit.
220    pub fn symbol(&self) -> String {
221        match self {
222            Unit::Base(b) => b.symbol.to_string(),
223            Unit::Composite(c) => c.to_string(),
224            Unit::Dimensionless { scale } => {
225                if (*scale - 1.0).abs() < 1e-15 {
226                    "".to_string()
227                } else {
228                    format!("{}", scale)
229                }
230            }
231        }
232    }
233}
234
235impl fmt::Display for Unit {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        match self {
238            Unit::Base(b) => write!(f, "{}", b.symbol),
239            Unit::Composite(c) => write!(f, "{}", c),
240            Unit::Dimensionless { scale } => {
241                if (*scale - 1.0).abs() < 1e-15 {
242                    write!(f, "dimensionless")
243                } else {
244                    write!(f, "{}", scale)
245                }
246            }
247        }
248    }
249}
250
251impl From<BaseUnit> for Unit {
252    fn from(b: BaseUnit) -> Unit {
253        Unit::Base(b)
254    }
255}
256
257impl From<&BaseUnit> for Unit {
258    fn from(b: &BaseUnit) -> Unit {
259        Unit::Base(*b)
260    }
261}
262
263impl From<&Unit> for Unit {
264    fn from(u: &Unit) -> Unit {
265        u.clone()
266    }
267}
268
269// Unit * Unit
270impl Mul for Unit {
271    type Output = Unit;
272
273    fn mul(self, rhs: Unit) -> Unit {
274        Unit::Composite(self.to_composite().mul(&rhs.to_composite()))
275    }
276}
277
278impl Mul for &Unit {
279    type Output = Unit;
280
281    fn mul(self, rhs: &Unit) -> Unit {
282        Unit::Composite(self.to_composite().mul(&rhs.to_composite()))
283    }
284}
285
286impl Mul<&Unit> for Unit {
287    type Output = Unit;
288
289    fn mul(self, rhs: &Unit) -> Unit {
290        Unit::Composite(self.to_composite().mul(&rhs.to_composite()))
291    }
292}
293
294impl Mul<Unit> for &Unit {
295    type Output = Unit;
296
297    fn mul(self, rhs: Unit) -> Unit {
298        Unit::Composite(self.to_composite().mul(&rhs.to_composite()))
299    }
300}
301
302// Unit / Unit
303impl Div for Unit {
304    type Output = Unit;
305
306    fn div(self, rhs: Unit) -> Unit {
307        Unit::Composite(self.to_composite().div(&rhs.to_composite()))
308    }
309}
310
311impl Div for &Unit {
312    type Output = Unit;
313
314    fn div(self, rhs: &Unit) -> Unit {
315        Unit::Composite(self.to_composite().div(&rhs.to_composite()))
316    }
317}
318
319impl Div<&Unit> for Unit {
320    type Output = Unit;
321
322    fn div(self, rhs: &Unit) -> Unit {
323        Unit::Composite(self.to_composite().div(&rhs.to_composite()))
324    }
325}
326
327impl Div<Unit> for &Unit {
328    type Output = Unit;
329
330    fn div(self, rhs: Unit) -> Unit {
331        Unit::Composite(self.to_composite().div(&rhs.to_composite()))
332    }
333}
334
335// BaseUnit * BaseUnit → Unit
336impl Mul for BaseUnit {
337    type Output = Unit;
338
339    fn mul(self, rhs: BaseUnit) -> Unit {
340        Unit::from(self) * Unit::from(rhs)
341    }
342}
343
344// BaseUnit / BaseUnit → Unit
345impl Div for BaseUnit {
346    type Output = Unit;
347
348    fn div(self, rhs: BaseUnit) -> Unit {
349        Unit::from(self) / Unit::from(rhs)
350    }
351}
352
353// BaseUnit * Unit → Unit
354impl Mul<Unit> for BaseUnit {
355    type Output = Unit;
356
357    fn mul(self, rhs: Unit) -> Unit {
358        Unit::from(self) * rhs
359    }
360}
361
362// Unit * BaseUnit → Unit
363impl Mul<BaseUnit> for Unit {
364    type Output = Unit;
365
366    fn mul(self, rhs: BaseUnit) -> Unit {
367        self * Unit::from(rhs)
368    }
369}
370
371// BaseUnit / Unit → Unit
372impl Div<Unit> for BaseUnit {
373    type Output = Unit;
374
375    fn div(self, rhs: Unit) -> Unit {
376        Unit::from(self) / rhs
377    }
378}
379
380// Unit / BaseUnit → Unit
381impl Div<BaseUnit> for Unit {
382    type Output = Unit;
383
384    fn div(self, rhs: BaseUnit) -> Unit {
385        self / Unit::from(rhs)
386    }
387}
388
389// &Unit * BaseUnit, &Unit / BaseUnit
390impl Mul<BaseUnit> for &Unit {
391    type Output = Unit;
392
393    fn mul(self, rhs: BaseUnit) -> Unit {
394        self * &Unit::from(rhs)
395    }
396}
397
398impl Div<BaseUnit> for &Unit {
399    type Output = Unit;
400
401    fn div(self, rhs: BaseUnit) -> Unit {
402        self / &Unit::from(rhs)
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    fn meter() -> Unit {
411        Unit::Base(BaseUnit::new("meter", "m", &[], Dimension::LENGTH, 1.0))
412    }
413
414    fn second() -> Unit {
415        Unit::Base(BaseUnit::new("second", "s", &[], Dimension::TIME, 1.0))
416    }
417
418    fn kilometer() -> Unit {
419        Unit::Base(BaseUnit::new(
420            "kilometer",
421            "km",
422            &[],
423            Dimension::LENGTH,
424            1000.0,
425        ))
426    }
427
428    #[test]
429    fn test_unit_division() {
430        let velocity = meter() / second();
431        let dim = velocity.dimension();
432        assert_eq!(dim.length, Rational16::ONE);
433        assert_eq!(dim.time, Rational16::new(-1, 1));
434    }
435
436    #[test]
437    fn test_conversion_factor() {
438        let m = meter();
439        let km = kilometer();
440        let factor = km.conversion_factor(&m).unwrap();
441        assert!((factor - 1000.0).abs() < 1e-10);
442    }
443
444    #[test]
445    fn test_incompatible_conversion() {
446        let m = meter();
447        let s = second();
448        let result = m.conversion_factor(&s);
449        assert!(matches!(result, Err(UnitError::DimensionMismatch { .. })));
450    }
451
452    #[test]
453    fn test_unit_power() {
454        let m = meter();
455        let m2 = m.pow(2);
456        let dim = m2.dimension();
457        assert_eq!(dim.length, Rational16::new(2, 1));
458    }
459
460    #[test]
461    fn test_unit_sqrt() {
462        let m = meter();
463        let m2 = &m * &m;
464        let sqrt_m2 = m2.sqrt();
465        let dim = sqrt_m2.dimension();
466        assert_eq!(dim.length, Rational16::ONE);
467    }
468
469    #[test]
470    fn test_offset_unit_conversion_factor_rejected() {
471        let kelvin = Unit::Base(BaseUnit::new(
472            "kelvin",
473            "K",
474            &[],
475            Dimension::TEMPERATURE,
476            1.0,
477        ));
478        let celsius = Unit::Base(BaseUnit::with_offset(
479            "celsius",
480            "°C",
481            &[],
482            Dimension::TEMPERATURE,
483            1.0,
484            273.15,
485        ));
486        let result = celsius.conversion_factor(&kelvin);
487        assert!(matches!(result, Err(UnitError::OffsetConversion { .. })));
488    }
489
490    #[test]
491    fn test_offset_unit_identity_conversion_ok() {
492        let celsius = Unit::Base(BaseUnit::with_offset(
493            "celsius",
494            "°C",
495            &[],
496            Dimension::TEMPERATURE,
497            1.0,
498            273.15,
499        ));
500        let result = celsius.conversion_factor(&celsius);
501        assert!(result.is_ok());
502        assert!((result.unwrap() - 1.0).abs() < 1e-15);
503    }
504
505    #[test]
506    fn test_pow_one_preserves_unit() {
507        let celsius = Unit::Base(BaseUnit::with_offset(
508            "celsius",
509            "°C",
510            &[],
511            Dimension::TEMPERATURE,
512            1.0,
513            273.15,
514        ));
515        let powered = celsius.pow(1);
516        assert!(powered.has_offset());
517        assert_eq!(celsius, powered);
518    }
519}