Skip to main content

dimvexa_core/
kind.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Quantity kinds, conversion shapes and exactness flags.
3
4use core::fmt;
5
6use crate::error::{Error, Result};
7
8/// The kind of a quantity on top of its dimension.
9///
10/// Two units with the same dimension but different kinds (for example `degC` and
11/// `delta_degC`) are not interchangeable. The kind algebra is:
12///
13/// | left | op | right | result |
14/// |---|---|---|---|
15/// | absolute | `-` | absolute | difference |
16/// | absolute | `+` | difference | absolute |
17/// | difference | `+` | absolute | absolute |
18/// | absolute | `-` | difference | absolute |
19/// | difference | `+`/`-` | difference | difference |
20/// | absolute | `+` | absolute | **error** |
21/// | difference | `-` | absolute | **error** |
22///
23/// # Examples
24///
25/// ```
26/// use dimvexa_core::Kind;
27///
28/// let abs = Kind::AbsoluteTemperature;
29/// let diff = Kind::TemperatureDifference;
30/// assert_eq!(abs.sub(abs)?, diff);
31/// assert_eq!(abs.add(diff)?, abs);
32/// assert!(abs.add(abs).is_err());
33/// # Ok::<(), dimvexa_core::Error>(())
34/// ```
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
36#[non_exhaustive]
37pub enum Kind {
38    /// An ordinary quantity without special rules.
39    #[default]
40    Plain,
41    /// A point on a temperature scale (`K`, `degC`, `degF`, `degR`).
42    AbsoluteTemperature,
43    /// An interval between two temperatures (`delta_K`, `delta_degC`, ...).
44    TemperatureDifference,
45}
46
47impl Kind {
48    /// The stable lower-case name used in data files and JSON output.
49    #[must_use]
50    pub const fn name(self) -> &'static str {
51        match self {
52            Self::Plain => "plain",
53            Self::AbsoluteTemperature => "absolute_temperature",
54            Self::TemperatureDifference => "temperature_difference",
55        }
56    }
57
58    /// Reads a name produced by [`Kind::name`].
59    #[must_use]
60    pub fn from_name(name: &str) -> Option<Self> {
61        match name {
62            "plain" => Some(Self::Plain),
63            "absolute_temperature" => Some(Self::AbsoluteTemperature),
64            "temperature_difference" => Some(Self::TemperatureDifference),
65            _ => None,
66        }
67    }
68
69    /// Kind of `self + rhs`.
70    ///
71    /// # Errors
72    ///
73    /// [`Error::KindMismatch`] for absolute + absolute and for mixing plain with
74    /// temperature kinds.
75    pub const fn add(self, rhs: Self) -> Result<Self> {
76        match (self, rhs) {
77            (Self::Plain, Self::Plain) => Ok(Self::Plain),
78            (Self::AbsoluteTemperature, Self::TemperatureDifference)
79            | (Self::TemperatureDifference, Self::AbsoluteTemperature) => {
80                Ok(Self::AbsoluteTemperature)
81            }
82            (Self::TemperatureDifference, Self::TemperatureDifference) => {
83                Ok(Self::TemperatureDifference)
84            }
85            _ => Err(Error::KindMismatch {
86                from: self,
87                to: rhs,
88                operation: "addition",
89                suggestions: Vec::new(),
90            }),
91        }
92    }
93
94    /// Kind of `self - rhs`.
95    ///
96    /// # Errors
97    ///
98    /// [`Error::KindMismatch`] for difference - absolute and for mixing plain with
99    /// temperature kinds.
100    pub const fn sub(self, rhs: Self) -> Result<Self> {
101        match (self, rhs) {
102            (Self::Plain, Self::Plain) => Ok(Self::Plain),
103            (Self::AbsoluteTemperature, Self::AbsoluteTemperature)
104            | (Self::TemperatureDifference, Self::TemperatureDifference) => {
105                Ok(Self::TemperatureDifference)
106            }
107            (Self::AbsoluteTemperature, Self::TemperatureDifference) => {
108                Ok(Self::AbsoluteTemperature)
109            }
110            _ => Err(Error::KindMismatch {
111                from: self,
112                to: rhs,
113                operation: "subtraction",
114                suggestions: Vec::new(),
115            }),
116        }
117    }
118}
119
120impl fmt::Display for Kind {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        f.write_str(self.name())
123    }
124}
125
126/// The shape of a conversion from a unit to its SI base: `base = factor * x + offset`.
127///
128/// M0 supports the two shapes that need no formulas. Logarithmic, reciprocal and formula
129/// scales arrive in later milestones.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131#[non_exhaustive]
132pub enum Shape {
133    /// `y = a * x`.
134    Linear,
135    /// `y = a * x + b` with a non-zero offset (temperature scales).
136    Affine,
137}
138
139impl Shape {
140    /// The stable lower-case name used in data files and JSON output.
141    #[must_use]
142    pub const fn name(self) -> &'static str {
143        match self {
144            Self::Linear => "linear",
145            Self::Affine => "affine",
146        }
147    }
148
149    /// Reads a name produced by [`Shape::name`].
150    #[must_use]
151    pub fn from_name(name: &str) -> Option<Self> {
152        match name {
153            "linear" => Some(Self::Linear),
154            "affine" => Some(Self::Affine),
155            _ => None,
156        }
157    }
158}
159
160impl fmt::Display for Shape {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        f.write_str(self.name())
163    }
164}
165
166/// Whether a factor is exact by definition or a measured value (ADR-002).
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
168#[non_exhaustive]
169pub enum Exactness {
170    /// Exact by definition (SI definition, statute, treaty or convention).
171    Defined,
172    /// A measured value, stored as the exact rational of the published decimal.
173    Measured,
174}
175
176impl Exactness {
177    /// The stable lower-case name used in data files and JSON output.
178    #[must_use]
179    pub const fn name(self) -> &'static str {
180        match self {
181            Self::Defined => "defined",
182            Self::Measured => "measured",
183        }
184    }
185
186    /// Reads a name produced by [`Exactness::name`].
187    #[must_use]
188    pub fn from_name(name: &str) -> Option<Self> {
189        match name {
190            "defined" => Some(Self::Defined),
191            "measured" => Some(Self::Measured),
192            _ => None,
193        }
194    }
195
196    /// The weaker of two flags: `Measured` if either is measured.
197    #[must_use]
198    pub const fn combine(self, other: Self) -> Self {
199        match (self, other) {
200            (Self::Defined, Self::Defined) => Self::Defined,
201            _ => Self::Measured,
202        }
203    }
204}
205
206impl fmt::Display for Exactness {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        f.write_str(self.name())
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    const ALL: [Kind; 3] = [
217        Kind::Plain,
218        Kind::AbsoluteTemperature,
219        Kind::TemperatureDifference,
220    ];
221
222    #[test]
223    fn names_round_trip() {
224        for k in ALL {
225            assert_eq!(Kind::from_name(k.name()), Some(k));
226        }
227        assert_eq!(Kind::from_name("nope"), None);
228        assert_eq!(Shape::from_name("affine"), Some(Shape::Affine));
229        assert_eq!(Exactness::from_name("measured"), Some(Exactness::Measured));
230    }
231
232    #[test]
233    fn abs_minus_abs_is_diff() {
234        let (a, d) = (Kind::AbsoluteTemperature, Kind::TemperatureDifference);
235        assert_eq!(a.sub(a), Ok(d));
236        assert_eq!(a.add(d), Ok(a));
237        assert_eq!(d.add(a), Ok(a));
238        assert_eq!(a.sub(d), Ok(a));
239        assert_eq!(d.add(d), Ok(d));
240        assert_eq!(d.sub(d), Ok(d));
241        assert!(a.add(a).is_err());
242        assert!(d.sub(a).is_err());
243    }
244
245    #[test]
246    fn plain_does_not_mix_with_temperature() {
247        for k in [Kind::AbsoluteTemperature, Kind::TemperatureDifference] {
248            assert!(Kind::Plain.add(k).is_err());
249            assert!(k.add(Kind::Plain).is_err());
250            assert!(Kind::Plain.sub(k).is_err());
251            assert!(k.sub(Kind::Plain).is_err());
252        }
253        assert_eq!(Kind::Plain.add(Kind::Plain), Ok(Kind::Plain));
254    }
255
256    #[test]
257    fn exactness_combine() {
258        assert_eq!(
259            Exactness::Defined.combine(Exactness::Defined),
260            Exactness::Defined
261        );
262        assert_eq!(
263            Exactness::Defined.combine(Exactness::Measured),
264            Exactness::Measured
265        );
266    }
267}