1use core::fmt;
5
6use crate::error::{Error, Result};
7
8const BASE_SYMBOLS: [&str; 7] = ["kg", "m", "s", "A", "K", "mol", "cd"];
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
30pub struct Dim([i8; 7]);
31
32impl Dim {
33 pub const NONE: Self = Self([0; 7]);
35 pub const KG: Self = Self([1, 0, 0, 0, 0, 0, 0]);
37 pub const M: Self = Self([0, 1, 0, 0, 0, 0, 0]);
39 pub const S: Self = Self([0, 0, 1, 0, 0, 0, 0]);
41 pub const A: Self = Self([0, 0, 0, 1, 0, 0, 0]);
43 pub const K: Self = Self([0, 0, 0, 0, 1, 0, 0]);
45 pub const MOL: Self = Self([0, 0, 0, 0, 0, 1, 0]);
47 pub const CD: Self = Self([0, 0, 0, 0, 0, 0, 1]);
49
50 #[must_use]
52 pub const fn new(exponents: [i8; 7]) -> Self {
53 Self(exponents)
54 }
55
56 #[must_use]
58 pub const fn exponents(self) -> [i8; 7] {
59 self.0
60 }
61
62 #[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 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 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 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 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}