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
6/// The kind of a quantity on top of its dimension.
7///
8/// Two units with the same dimension but different kinds (for example `degC` and
9/// `delta_degC`) are not interchangeable: a conversion between them is refused with
10/// [`Error::KindMismatch`](crate::Error::KindMismatch). A temperature point (`degC`) converts to another point (`K`), and a
11/// temperature difference (`delta_degF`) converts to another difference (`delta_K`). dimvexa
12/// does not add or subtract quantities (ADR-014), so a kind has no arithmetic.
13///
14/// # Examples
15///
16/// ```
17/// use dimvexa_core::Kind;
18///
19/// assert_eq!(Kind::from_name("absolute_temperature"), Some(Kind::AbsoluteTemperature));
20/// assert_eq!(Kind::TemperatureDifference.name(), "temperature_difference");
21/// ```
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
23#[non_exhaustive]
24pub enum Kind {
25    /// An ordinary quantity without special rules.
26    #[default]
27    Plain,
28    /// A point on a temperature scale (`K`, `degC`, `degF`, `degR`).
29    AbsoluteTemperature,
30    /// An interval between two temperatures (`delta_K`, `delta_degC`, ...).
31    TemperatureDifference,
32}
33
34impl Kind {
35    /// The stable lower-case name used in data files and JSON output.
36    #[must_use]
37    pub const fn name(self) -> &'static str {
38        match self {
39            Self::Plain => "plain",
40            Self::AbsoluteTemperature => "absolute_temperature",
41            Self::TemperatureDifference => "temperature_difference",
42        }
43    }
44
45    /// Reads a name produced by [`Kind::name`].
46    #[must_use]
47    pub fn from_name(name: &str) -> Option<Self> {
48        match name {
49            "plain" => Some(Self::Plain),
50            "absolute_temperature" => Some(Self::AbsoluteTemperature),
51            "temperature_difference" => Some(Self::TemperatureDifference),
52            _ => None,
53        }
54    }
55}
56
57impl fmt::Display for Kind {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        f.write_str(self.name())
60    }
61}
62
63/// The shape of a conversion from a unit to its SI base: `base = factor * x + offset`.
64///
65/// M0 supports the two shapes that need no formulas. Logarithmic, reciprocal and formula
66/// scales arrive in later milestones.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68#[non_exhaustive]
69pub enum Shape {
70    /// `y = a * x`.
71    Linear,
72    /// `y = a * x + b` with a non-zero offset (temperature scales).
73    Affine,
74}
75
76impl Shape {
77    /// The stable lower-case name used in data files and JSON output.
78    #[must_use]
79    pub const fn name(self) -> &'static str {
80        match self {
81            Self::Linear => "linear",
82            Self::Affine => "affine",
83        }
84    }
85
86    /// Reads a name produced by [`Shape::name`].
87    #[must_use]
88    pub fn from_name(name: &str) -> Option<Self> {
89        match name {
90            "linear" => Some(Self::Linear),
91            "affine" => Some(Self::Affine),
92            _ => None,
93        }
94    }
95}
96
97impl fmt::Display for Shape {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        f.write_str(self.name())
100    }
101}
102
103/// Whether a factor is exact by definition or a measured value (ADR-002).
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105#[non_exhaustive]
106pub enum Exactness {
107    /// Exact by definition (SI definition, statute, treaty or convention).
108    Defined,
109    /// A measured value, stored as the exact rational of the published decimal.
110    Measured,
111}
112
113impl Exactness {
114    /// The stable lower-case name used in data files and JSON output.
115    #[must_use]
116    pub const fn name(self) -> &'static str {
117        match self {
118            Self::Defined => "defined",
119            Self::Measured => "measured",
120        }
121    }
122
123    /// Reads a name produced by [`Exactness::name`].
124    #[must_use]
125    pub fn from_name(name: &str) -> Option<Self> {
126        match name {
127            "defined" => Some(Self::Defined),
128            "measured" => Some(Self::Measured),
129            _ => None,
130        }
131    }
132
133    /// The weaker of two flags: `Measured` if either is measured.
134    #[must_use]
135    pub const fn combine(self, other: Self) -> Self {
136        match (self, other) {
137            (Self::Defined, Self::Defined) => Self::Defined,
138            _ => Self::Measured,
139        }
140    }
141}
142
143impl fmt::Display for Exactness {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        f.write_str(self.name())
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    const ALL: [Kind; 3] = [
154        Kind::Plain,
155        Kind::AbsoluteTemperature,
156        Kind::TemperatureDifference,
157    ];
158
159    #[test]
160    fn names_round_trip() {
161        for k in ALL {
162            assert_eq!(Kind::from_name(k.name()), Some(k));
163        }
164        assert_eq!(Kind::from_name("nope"), None);
165        assert_eq!(Shape::from_name("affine"), Some(Shape::Affine));
166        assert_eq!(Exactness::from_name("measured"), Some(Exactness::Measured));
167    }
168
169    #[test]
170    fn exactness_combine() {
171        assert_eq!(
172            Exactness::Defined.combine(Exactness::Defined),
173            Exactness::Defined
174        );
175        assert_eq!(
176            Exactness::Defined.combine(Exactness::Measured),
177            Exactness::Measured
178        );
179    }
180}