Skip to main content

dimvexa_core/
prefix.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! SI and binary prefixes.
3
4use crate::error::Result;
5use crate::ratio::Ratio;
6
7/// Which prefixes a unit accepts.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
9#[non_exhaustive]
10pub enum PrefixMode {
11    /// No prefixes (for example `min`, `psi`, `degC`).
12    #[default]
13    None,
14    /// The 24 SI prefixes from quecto to quetta.
15    Si,
16    /// SI and binary prefixes (`Ki`, `Mi`, ...), for information units.
17    SiAndBinary,
18}
19
20impl PrefixMode {
21    /// The stable lower-case name used in data files.
22    #[must_use]
23    pub const fn name(self) -> &'static str {
24        match self {
25            Self::None => "none",
26            Self::Si => "si",
27            Self::SiAndBinary => "si_binary",
28        }
29    }
30
31    /// Reads a name produced by [`PrefixMode::name`].
32    #[must_use]
33    pub fn from_name(name: &str) -> Option<Self> {
34        match name {
35            "none" => Some(Self::None),
36            "si" => Some(Self::Si),
37            "si_binary" => Some(Self::SiAndBinary),
38            _ => None,
39        }
40    }
41
42    /// True if a prefix of the given family is accepted.
43    #[must_use]
44    pub const fn accepts(self, family: PrefixFamily) -> bool {
45        matches!(
46            (self, family),
47            (Self::Si | Self::SiAndBinary, PrefixFamily::Si)
48                | (Self::SiAndBinary, PrefixFamily::Binary)
49        )
50    }
51}
52
53/// The family a prefix belongs to.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55#[non_exhaustive]
56pub enum PrefixFamily {
57    /// Decimal SI prefixes, factor `10^exp`.
58    Si,
59    /// Binary prefixes, factor `2^exp`.
60    Binary,
61}
62
63/// A prefix such as `k` (kilo) or `Mi` (mebi).
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct Prefix {
66    /// The symbol as written in unit expressions.
67    pub symbol: &'static str,
68    /// The prefix name.
69    pub name: &'static str,
70    /// Decimal or binary family.
71    pub family: PrefixFamily,
72    /// Exponent of 10 (SI) or of 2 (binary).
73    pub exp: i32,
74}
75
76impl Prefix {
77    /// The exact multiplication factor.
78    ///
79    /// # Errors
80    ///
81    /// [`crate::Error::Overflow`] cannot occur for the built-in tables (10^30 and 2^80 fit
82    /// in `i128`) but is reported rather than assumed.
83    pub fn factor(self) -> Result<Ratio> {
84        let base = match self.family {
85            PrefixFamily::Si => 10,
86            PrefixFamily::Binary => 2,
87        };
88        Ratio::from_int(base).checked_pow(self.exp)
89    }
90}
91
92const fn si(symbol: &'static str, name: &'static str, exp: i32) -> Prefix {
93    Prefix {
94        symbol,
95        name,
96        family: PrefixFamily::Si,
97        exp,
98    }
99}
100
101const fn bin(symbol: &'static str, name: &'static str, exp: i32) -> Prefix {
102    Prefix {
103        symbol,
104        name,
105        family: PrefixFamily::Binary,
106        exp,
107    }
108}
109
110/// All 24 SI prefixes (ronna and quetta, ronto and quecto included, as adopted in 2022),
111/// plus the ASCII `u` and the Greek-mu spellings `\u{b5}` and `\u{3bc}` of micro.
112pub const SI_PREFIXES: [Prefix; 26] = [
113    si("Q", "quetta", 30),
114    si("R", "ronna", 27),
115    si("Y", "yotta", 24),
116    si("Z", "zetta", 21),
117    si("E", "exa", 18),
118    si("P", "peta", 15),
119    si("T", "tera", 12),
120    si("G", "giga", 9),
121    si("M", "mega", 6),
122    si("k", "kilo", 3),
123    si("h", "hecto", 2),
124    si("da", "deca", 1),
125    si("d", "deci", -1),
126    si("c", "centi", -2),
127    si("m", "milli", -3),
128    si("\u{b5}", "micro", -6),
129    si("\u{3bc}", "micro", -6),
130    si("u", "micro", -6),
131    si("n", "nano", -9),
132    si("p", "pico", -12),
133    si("f", "femto", -15),
134    si("a", "atto", -18),
135    si("z", "zepto", -21),
136    si("y", "yocto", -24),
137    si("r", "ronto", -27),
138    si("q", "quecto", -30),
139];
140
141/// The eight binary prefixes kibi to yobi.
142pub const BINARY_PREFIXES: [Prefix; 8] = [
143    bin("Ki", "kibi", 10),
144    bin("Mi", "mebi", 20),
145    bin("Gi", "gibi", 30),
146    bin("Ti", "tebi", 40),
147    bin("Pi", "pebi", 50),
148    bin("Ei", "exbi", 60),
149    bin("Zi", "zebi", 70),
150    bin("Yi", "yobi", 80),
151];
152
153/// Iterates over every known prefix (SI first, then binary).
154pub fn all_prefixes() -> impl Iterator<Item = Prefix> {
155    SI_PREFIXES.iter().chain(BINARY_PREFIXES.iter()).copied()
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use std::collections::BTreeSet;
162
163    #[test]
164    fn there_are_24_distinct_si_prefixes() {
165        let exps: BTreeSet<i32> = SI_PREFIXES.iter().map(|p| p.exp).collect();
166        // 24 prefixes: 10^-30 .. 10^30 (the 24 named steps, no 10^0).
167        assert_eq!(exps.len(), 24);
168        let names: BTreeSet<&str> = SI_PREFIXES.iter().map(|p| p.name).collect();
169        assert_eq!(names.len(), 24);
170        assert!(names.contains("ronna") && names.contains("quetta"));
171        assert!(names.contains("ronto") && names.contains("quecto"));
172    }
173
174    #[test]
175    fn factors_are_exact() {
176        let q = SI_PREFIXES[0];
177        assert_eq!(q.factor().unwrap(), Ratio::from_int(10_i128.pow(30)));
178        let quecto = SI_PREFIXES[25];
179        assert_eq!(
180            quecto.factor().unwrap(),
181            Ratio::new(1, 10_i128.pow(30)).unwrap()
182        );
183        let yobi = BINARY_PREFIXES[7];
184        assert_eq!(yobi.factor().unwrap(), Ratio::from_int(1_i128 << 80));
185    }
186
187    #[test]
188    fn mode_names() {
189        for m in [PrefixMode::None, PrefixMode::Si, PrefixMode::SiAndBinary] {
190            assert_eq!(PrefixMode::from_name(m.name()), Some(m));
191        }
192        assert!(PrefixMode::Si.accepts(PrefixFamily::Si));
193        assert!(!PrefixMode::Si.accepts(PrefixFamily::Binary));
194        assert!(!PrefixMode::None.accepts(PrefixFamily::Si));
195        assert!(PrefixMode::SiAndBinary.accepts(PrefixFamily::Binary));
196    }
197}