Skip to main content

dimvexa_core/
dim.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Physical dimension vectors over the seven SI base quantities.
3
4use core::fmt;
5
6use crate::error::{Error, Result};
7
8/// Symbols of the seven base units, in vector order.
9const BASE_SYMBOLS: [&str; 7] = ["kg", "m", "s", "A", "K", "mol", "cd"];
10
11/// A dimension: integer exponents of kilogram, metre, second, ampere, kelvin, mole and
12/// candela, in that order.
13///
14/// The mass base is the kilogram, not the gram. Plane angle is dimensionless (see ADR-007).
15/// All arithmetic is checked: exponent overflow is reported as [`Error::Overflow`].
16///
17/// # Examples
18///
19/// ```
20/// use dimvexa_core::Dim;
21///
22/// let velocity = Dim::M.checked_sub(Dim::S)?;
23/// assert_eq!(velocity.exponents(), [0, 1, -1, 0, 0, 0, 0]);
24/// let accel = velocity.checked_sub(Dim::S)?;
25/// let force = Dim::KG.checked_add(accel)?;
26/// assert_eq!(force.to_string(), "kg m s^-2");
27/// # Ok::<(), dimvexa_core::Error>(())
28/// ```
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
30pub struct Dim([i8; 7]);
31
32impl Dim {
33    /// The dimensionless vector (all exponents zero).
34    pub const NONE: Self = Self([0; 7]);
35    /// Mass (kilogram).
36    pub const KG: Self = Self([1, 0, 0, 0, 0, 0, 0]);
37    /// Length (metre).
38    pub const M: Self = Self([0, 1, 0, 0, 0, 0, 0]);
39    /// Time (second).
40    pub const S: Self = Self([0, 0, 1, 0, 0, 0, 0]);
41    /// Electric current (ampere).
42    pub const A: Self = Self([0, 0, 0, 1, 0, 0, 0]);
43    /// Thermodynamic temperature (kelvin).
44    pub const K: Self = Self([0, 0, 0, 0, 1, 0, 0]);
45    /// Amount of substance (mole).
46    pub const MOL: Self = Self([0, 0, 0, 0, 0, 1, 0]);
47    /// Luminous intensity (candela).
48    pub const CD: Self = Self([0, 0, 0, 0, 0, 0, 1]);
49
50    /// Builds a dimension from exponents of `[kg, m, s, A, K, mol, cd]`.
51    #[must_use]
52    pub const fn new(exponents: [i8; 7]) -> Self {
53        Self(exponents)
54    }
55
56    /// The exponents of `[kg, m, s, A, K, mol, cd]`.
57    #[must_use]
58    pub const fn exponents(self) -> [i8; 7] {
59        self.0
60    }
61
62    /// True if every exponent is zero.
63    #[must_use]
64    pub fn is_dimensionless(self) -> bool {
65        self == Self::NONE
66    }
67
68    #[inline]
69    fn zip_with(self, other: Self, f: impl Fn(i8, i8) -> Option<i8>) -> Option<Self> {
70        let [a0, a1, a2, a3, a4, a5, a6] = self.0;
71        let [b0, b1, b2, b3, b4, b5, b6] = other.0;
72        Some(Self([
73            f(a0, b0)?,
74            f(a1, b1)?,
75            f(a2, b2)?,
76            f(a3, b3)?,
77            f(a4, b4)?,
78            f(a5, b5)?,
79            f(a6, b6)?,
80        ]))
81    }
82
83    /// Dimension of a product: exponents add.
84    ///
85    /// # Errors
86    ///
87    /// [`Error::Overflow`] if an exponent leaves the `i8` range.
88    pub fn checked_add(self, other: Self) -> Result<Self> {
89        self.zip_with(other, i8::checked_add)
90            .ok_or(Error::Overflow {
91                operation: "dimension add",
92            })
93    }
94
95    /// Dimension of a quotient: exponents subtract.
96    ///
97    /// # Errors
98    ///
99    /// [`Error::Overflow`] if an exponent leaves the `i8` range.
100    pub fn checked_sub(self, other: Self) -> Result<Self> {
101        self.zip_with(other, i8::checked_sub)
102            .ok_or(Error::Overflow {
103                operation: "dimension subtract",
104            })
105    }
106
107    /// Dimension of a power: every exponent is multiplied by `n`.
108    ///
109    /// # Errors
110    ///
111    /// [`Error::Overflow`] if an exponent leaves the `i8` range.
112    pub fn checked_scale(self, n: i32) -> Result<Self> {
113        let overflow = || Error::Overflow {
114            operation: "dimension scale",
115        };
116        let n = i8::try_from(n).map_err(|_| overflow())?;
117        let [a0, a1, a2, a3, a4, a5, a6] = self.0;
118        let m = |a: i8| a.checked_mul(n);
119        Ok(Self([
120            m(a0).ok_or_else(overflow)?,
121            m(a1).ok_or_else(overflow)?,
122            m(a2).ok_or_else(overflow)?,
123            m(a3).ok_or_else(overflow)?,
124            m(a4).ok_or_else(overflow)?,
125            m(a5).ok_or_else(overflow)?,
126            m(a6).ok_or_else(overflow)?,
127        ]))
128    }
129
130    /// Dimension of the reciprocal: every exponent is negated.
131    ///
132    /// # Errors
133    ///
134    /// [`Error::Overflow`] if an exponent is `i8::MIN`.
135    pub fn checked_neg(self) -> Result<Self> {
136        Self::NONE.checked_sub(self)
137    }
138}
139
140impl fmt::Display for Dim {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        if self.is_dimensionless() {
143            return f.write_str("1");
144        }
145        let mut first = true;
146        for (sym, exp) in BASE_SYMBOLS.iter().zip(self.0.iter()) {
147            if *exp == 0 {
148                continue;
149            }
150            if !first {
151                f.write_str(" ")?;
152            }
153            first = false;
154            if *exp == 1 {
155                f.write_str(sym)?;
156            } else {
157                write!(f, "{sym}^{exp}")?;
158            }
159        }
160        Ok(())
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn add_sub_inverse() {
170        let a = Dim::new([1, 2, -3, 0, 0, 0, 0]);
171        let b = Dim::new([0, -1, 4, 1, 0, 0, 0]);
172        assert_eq!(a.checked_add(b).and_then(|c| c.checked_sub(b)), Ok(a));
173    }
174
175    #[test]
176    fn overflow_is_reported() {
177        let big = Dim::new([100, 0, 0, 0, 0, 0, 0]);
178        assert!(big.checked_add(big).is_err());
179        assert!(big.checked_scale(2).is_err());
180        assert!(big.checked_scale(1000).is_err());
181        assert!(Dim::new([i8::MIN, 0, 0, 0, 0, 0, 0]).checked_neg().is_err());
182    }
183
184    #[test]
185    fn display() {
186        assert_eq!(Dim::NONE.to_string(), "1");
187        assert_eq!(Dim::new([1, 2, -2, 0, 0, 0, 0]).to_string(), "kg m^2 s^-2");
188    }
189
190    #[test]
191    fn scale_by_zero_is_dimensionless() {
192        assert_eq!(Dim::M.checked_scale(0), Ok(Dim::NONE));
193    }
194}