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    /// A pressure read relative to the (unspecified) ambient pressure: `psig`, `barg`, `Pag`
33    /// (ADR-013). It converts only to another gauge unit.
34    GaugePressure,
35    /// A gas volume at the normal state of DIN 1343 (273.15 K, 101 325 Pa): `Nm3` (ADR-013).
36    NormalVolume,
37    /// A gas volume at 60 degF and 14.696 psia: `scf` (ADR-013).
38    StandardVolume,
39}
40
41impl Kind {
42    /// True for a kind that is tied to a stated reference state (ADR-013). Such a unit
43    /// converts only to a unit of the same kind, and the kind survives in compound units
44    /// (`Nm3/h` keeps the reference of `Nm3`).
45    #[must_use]
46    pub const fn is_reference(self) -> bool {
47        matches!(
48            self,
49            Self::GaugePressure | Self::NormalVolume | Self::StandardVolume
50        )
51    }
52
53    /// The stable lower-case name used in data files and JSON output.
54    #[must_use]
55    pub const fn name(self) -> &'static str {
56        match self {
57            Self::Plain => "plain",
58            Self::AbsoluteTemperature => "absolute_temperature",
59            Self::TemperatureDifference => "temperature_difference",
60            Self::GaugePressure => "gauge_pressure",
61            Self::NormalVolume => "normal_volume",
62            Self::StandardVolume => "standard_volume",
63        }
64    }
65
66    /// Reads a name produced by [`Kind::name`].
67    #[must_use]
68    pub fn from_name(name: &str) -> Option<Self> {
69        match name {
70            "plain" => Some(Self::Plain),
71            "absolute_temperature" => Some(Self::AbsoluteTemperature),
72            "temperature_difference" => Some(Self::TemperatureDifference),
73            "gauge_pressure" => Some(Self::GaugePressure),
74            "normal_volume" => Some(Self::NormalVolume),
75            "standard_volume" => Some(Self::StandardVolume),
76            _ => None,
77        }
78    }
79}
80
81impl fmt::Display for Kind {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        f.write_str(self.name())
84    }
85}
86
87/// The shape of a conversion from a unit to its SI base: `base = factor * x + offset`.
88///
89/// M0 supports the two shapes that need no formulas. Logarithmic, reciprocal and formula
90/// scales arrive in later milestones.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92#[non_exhaustive]
93pub enum Shape {
94    /// `y = a * x`.
95    Linear,
96    /// `y = a * x + b` with a non-zero offset (temperature scales).
97    Affine,
98}
99
100impl Shape {
101    /// The stable lower-case name used in data files and JSON output.
102    #[must_use]
103    pub const fn name(self) -> &'static str {
104        match self {
105            Self::Linear => "linear",
106            Self::Affine => "affine",
107        }
108    }
109
110    /// Reads a name produced by [`Shape::name`].
111    #[must_use]
112    pub fn from_name(name: &str) -> Option<Self> {
113        match name {
114            "linear" => Some(Self::Linear),
115            "affine" => Some(Self::Affine),
116            _ => None,
117        }
118    }
119}
120
121impl fmt::Display for Shape {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        f.write_str(self.name())
124    }
125}
126
127/// Whether a factor is exact by definition or a measured value (ADR-002).
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
129#[non_exhaustive]
130pub enum Exactness {
131    /// Exact by definition (SI definition, statute, treaty or convention).
132    Defined,
133    /// A measured value, stored as the exact rational of the published decimal.
134    Measured,
135}
136
137impl Exactness {
138    /// The stable lower-case name used in data files and JSON output.
139    #[must_use]
140    pub const fn name(self) -> &'static str {
141        match self {
142            Self::Defined => "defined",
143            Self::Measured => "measured",
144        }
145    }
146
147    /// Reads a name produced by [`Exactness::name`].
148    #[must_use]
149    pub fn from_name(name: &str) -> Option<Self> {
150        match name {
151            "defined" => Some(Self::Defined),
152            "measured" => Some(Self::Measured),
153            _ => None,
154        }
155    }
156
157    /// The weaker of two flags: `Measured` if either is measured.
158    #[must_use]
159    pub const fn combine(self, other: Self) -> Self {
160        match (self, other) {
161            (Self::Defined, Self::Defined) => Self::Defined,
162            _ => Self::Measured,
163        }
164    }
165}
166
167impl fmt::Display for Exactness {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        f.write_str(self.name())
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    const ALL: [Kind; 6] = [
178        Kind::Plain,
179        Kind::AbsoluteTemperature,
180        Kind::TemperatureDifference,
181        Kind::GaugePressure,
182        Kind::NormalVolume,
183        Kind::StandardVolume,
184    ];
185
186    #[test]
187    fn names_round_trip() {
188        for k in ALL {
189            assert_eq!(Kind::from_name(k.name()), Some(k));
190        }
191        assert_eq!(Kind::from_name("nope"), None);
192        assert_eq!(Shape::from_name("affine"), Some(Shape::Affine));
193        assert_eq!(Exactness::from_name("measured"), Some(Exactness::Measured));
194    }
195
196    #[test]
197    fn exactness_combine() {
198        assert_eq!(
199            Exactness::Defined.combine(Exactness::Defined),
200            Exactness::Defined
201        );
202        assert_eq!(
203            Exactness::Defined.combine(Exactness::Measured),
204            Exactness::Measured
205        );
206    }
207}