Skip to main content

irox_units/
prefixes.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2025 IROX Contributors
3//
4
5use core::cmp::Ordering;
6use irox_tools::cfg_feature_alloc;
7
8#[allow(unused_imports)]
9use irox_tools::f64::FloatExt;
10
11cfg_feature_alloc! {
12    extern crate alloc;
13    use alloc::format;
14}
15
16#[derive(Debug, Copy, Clone)]
17pub struct SIPrefix {
18    name: &'static str,
19    symbol: &'static str,
20    base_exponent: i8,
21    scale_factor: f64,
22}
23impl PartialEq for SIPrefix {
24    fn eq(&self, other: &Self) -> bool {
25        self.base_exponent == other.base_exponent
26    }
27}
28impl Eq for SIPrefix {}
29impl PartialOrd for SIPrefix {
30    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
31        Some(self.cmp(other))
32    }
33}
34impl Ord for SIPrefix {
35    fn cmp(&self, other: &Self) -> Ordering {
36        self.base_exponent.cmp(&other.base_exponent)
37    }
38}
39impl SIPrefix {
40    #[must_use]
41    pub const fn new(
42        name: &'static str,
43        symbol: &'static str,
44        base_exponent: i8,
45        scale_factor: f64,
46    ) -> SIPrefix {
47        Self {
48            name,
49            symbol,
50            base_exponent,
51            scale_factor,
52        }
53    }
54    #[must_use]
55    pub const fn base_exponent(&self) -> i8 {
56        self.base_exponent
57    }
58    #[must_use]
59    pub const fn name(&self) -> &'static str {
60        self.name
61    }
62    #[must_use]
63    pub const fn symbol(&self) -> &'static str {
64        self.symbol
65    }
66    #[must_use]
67    pub const fn scale_factor(&self) -> f64 {
68        self.scale_factor
69    }
70
71    cfg_feature_alloc! {
72        pub fn format<T: irox_tools::ToF64>(&self, t: &T) -> alloc::string::String {
73            let val = t.to_f64() / self.scale_factor;
74            format!("{val:.3}{}", self.symbol)
75        }
76        pub fn format_args<T: irox_tools::ToF64>(&self, fmt: PrefixFormat, t: &T) -> alloc::string::String {
77            let val = t.to_f64() / self.scale_factor;
78
79            format!("{val:precision$.width$}{}", self.symbol, width = fmt.width, precision = fmt.precision)
80        }
81    }
82
83    pub fn display<T: irox_tools::ToF64>(
84        &self,
85        t: &T,
86        f: &mut core::fmt::Formatter<'_>,
87    ) -> core::fmt::Result {
88        let val = t.to_f64() / self.scale_factor;
89        core::write!(f, "{val:.3}{}", self.symbol)
90    }
91}
92
93#[derive(Debug, Clone, Copy, Default)]
94pub struct PrefixFormat {
95    width: usize,
96    precision: usize,
97}
98impl PrefixFormat {
99    #[must_use]
100    pub fn new() -> Self {
101        Self {
102            precision: 0,
103            width: 0,
104        }
105    }
106    #[must_use]
107    pub fn with_width(mut self, width: usize) -> Self {
108        self.width = width;
109        self
110    }
111    #[must_use]
112    pub fn with_precision(mut self, precision: usize) -> Self {
113        self.precision = precision;
114        self
115    }
116}
117
118pub const QUETTA: SIPrefix = SIPrefix::new("quetta", "Q", 30, 1e30);
119pub const RONNA: SIPrefix = SIPrefix::new("ronna", "R", 27, 1e27);
120pub const YOTTA: SIPrefix = SIPrefix::new("yotta", "Y", 24, 1e24);
121pub const ZETTA: SIPrefix = SIPrefix::new("zeta", "Z", 21, 1e21);
122pub const EXA: SIPrefix = SIPrefix::new("exa", "E", 18, 1e18);
123pub const PETA: SIPrefix = SIPrefix::new("peta", "P", 15, 1e15);
124pub const TERA: SIPrefix = SIPrefix::new("tera", "T", 12, 1e12);
125pub const GIGA: SIPrefix = SIPrefix::new("giga", "G", 9, 1e9);
126pub const MEGA: SIPrefix = SIPrefix::new("mega", "M", 6, 1e6);
127pub const KILO: SIPrefix = SIPrefix::new("kilo", "k", 3, 1e3);
128pub const HECTO: SIPrefix = SIPrefix::new("hecto", "h", 2, 1e2);
129pub const DECA: SIPrefix = SIPrefix::new("deca", "da", 1, 1e1);
130pub const DECI: SIPrefix = SIPrefix::new("deci", "d", -1, 1e-1);
131pub const CENTI: SIPrefix = SIPrefix::new("centi", "c", -2, 1e-2);
132pub const MILLI: SIPrefix = SIPrefix::new("milli", "m", -3, 1e-3);
133pub const MICRO: SIPrefix = SIPrefix::new("micro", "\u{03BC}", -6, 1e-6);
134pub const NANO: SIPrefix = SIPrefix::new("nano", "n", -9, 1e-9);
135pub const PICO: SIPrefix = SIPrefix::new("pico", "p", -12, 1e-12);
136pub const FEMTO: SIPrefix = SIPrefix::new("femto", "f", -15, 1e-15);
137pub const ATTO: SIPrefix = SIPrefix::new("atto", "a", -18, 1e-18);
138pub const ZEPTO: SIPrefix = SIPrefix::new("zepto", "z", -21, 1e-21);
139pub const YOCTO: SIPrefix = SIPrefix::new("yocto", "y", -24, 1e-24);
140pub const RONTO: SIPrefix = SIPrefix::new("ronto", "r", -27, 1e-27);
141pub const QUECTO: SIPrefix = SIPrefix::new("quecto", "q", -30, 1e-30);
142
143/// All SI Prefixes
144pub const ALL_PREFIXES: &[SIPrefix] = &[
145    QUETTA, RONNA, YOTTA, ZETTA, EXA, PETA, TERA, GIGA, MEGA, KILO, HECTO, DECA, DECI, CENTI,
146    MILLI, MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO, RONTO, QUECTO,
147];
148/// Only the common power-of-three prefixes. Excludes [`QUETTA`], [`RONNA`], [`HECTO`], [`DECA`], [`DECI`], [`CENTI`], [`RONTO`], [`QUECTO`]
149pub const COMMON_PREFIXES: &[SIPrefix] = &[
150    YOTTA, ZETTA, EXA, PETA, TERA, GIGA, MEGA, KILO, MILLI, MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO,
151    YOCTO,
152];
153
154#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
155pub enum PrefixSet {
156    All,
157    #[default]
158    Common,
159}
160impl PrefixSet {
161    #[must_use]
162    pub fn prefixes(&self) -> &'static [SIPrefix] {
163        match self {
164            Self::All => ALL_PREFIXES,
165            Self::Common => COMMON_PREFIXES,
166        }
167    }
168    pub fn best_prefix_for<T: irox_tools::ToF64>(&self, t: &T) -> Option<SIPrefix> {
169        let v = t.to_f64().abs();
170        let e = v.log10();
171        if (0. ..1.).contains(&e) {
172            return None;
173        }
174        let mut last_matched = None;
175        let fixes: &'static [SIPrefix] = self.prefixes();
176        for prefix in fixes {
177            let exp = prefix.base_exponent as f64;
178
179            last_matched = Some(*prefix);
180            if exp <= e {
181                break;
182            }
183        }
184        if let Some(lm) = last_matched {
185            let var = e - lm.base_exponent as f64;
186            if !(0. ..3.).contains(&var) {
187                return None;
188            }
189        }
190        last_matched
191    }
192}
193
194#[cfg(test)]
195mod test {
196    use crate::prefixes::{
197        PrefixSet, ATTO, CENTI, DECA, DECI, EXA, FEMTO, GIGA, HECTO, KILO, MEGA, MICRO, MILLI,
198        NANO, PETA, PICO, QUECTO, QUETTA, RONNA, RONTO, TERA, YOCTO, YOTTA, ZEPTO, ZETTA,
199    };
200
201    macro_rules! impl_test {
202        ($name:ident, $com:expr, $v:literal, $all:expr) => {
203            #[test]
204            pub fn $name() {
205                assert_eq!($com, PrefixSet::Common.best_prefix_for(&$v), "{:e}", $v);
206                assert_eq!($all, PrefixSet::All.best_prefix_for(&$v), "{:e}", $v);
207
208                let v: f64 = ($v as f64).abs();
209                let f = 10f64.powf(v.log10() + 0.3f64);
210                assert_eq!($com, PrefixSet::Common.best_prefix_for(&f), "{v:e} {f:e}");
211                let f = 10f64.powf(v.log10() + 0.7f64);
212                assert_eq!($com, PrefixSet::Common.best_prefix_for(&f), "{v:e} {f:e}");
213            }
214        };
215    }
216    impl_test!(test_quecto_30, None, 1e-30, Some(QUECTO));
217    impl_test!(test_quecto_29, None, 1e-29, Some(QUECTO));
218    impl_test!(test_quecto_28, None, 1e-28, Some(QUECTO));
219
220    impl_test!(test_ronto_27, None, 1e-27, Some(RONTO));
221    impl_test!(test_ronto_26, None, 1e-26, Some(RONTO));
222    impl_test!(test_ronto_25, None, 1e-25, Some(RONTO));
223
224    impl_test!(test_yocto_24, Some(YOCTO), 1e-24, Some(YOCTO));
225    impl_test!(test_yocto_23, Some(YOCTO), 1e-23, Some(YOCTO));
226    impl_test!(test_yocto_22, Some(YOCTO), 1e-22, Some(YOCTO));
227
228    impl_test!(test_zepto_21, Some(ZEPTO), 1e-21, Some(ZEPTO));
229    impl_test!(test_zepto_20, Some(ZEPTO), 1e-20, Some(ZEPTO));
230    impl_test!(test_zepto_19, Some(ZEPTO), 1e-19, Some(ZEPTO));
231
232    impl_test!(test_atto_18, Some(ATTO), 1e-18, Some(ATTO));
233    impl_test!(test_atto_17, Some(ATTO), 1e-17, Some(ATTO));
234    impl_test!(test_atto_16, Some(ATTO), 1e-16, Some(ATTO));
235
236    impl_test!(test_femto_15, Some(FEMTO), 1e-15, Some(FEMTO));
237    impl_test!(test_femto_14, Some(FEMTO), 1e-14, Some(FEMTO));
238    impl_test!(test_femto_13, Some(FEMTO), 1e-13, Some(FEMTO));
239
240    impl_test!(test_pico_12, Some(PICO), 1e-12, Some(PICO));
241    impl_test!(test_pico_11, Some(PICO), 1e-11, Some(PICO));
242    impl_test!(test_pico_10, Some(PICO), 1e-10, Some(PICO));
243
244    impl_test!(test_nano_09, Some(NANO), 1e-9, Some(NANO));
245    impl_test!(test_nano_08, Some(NANO), 1e-8, Some(NANO));
246    impl_test!(test_nano_07, Some(NANO), 1e-7, Some(NANO));
247
248    impl_test!(test_micro_06, Some(MICRO), 1e-6, Some(MICRO));
249    impl_test!(test_micro_m06, Some(MICRO), -1e-6, Some(MICRO));
250    impl_test!(test_micro_05, Some(MICRO), 1e-5, Some(MICRO));
251    impl_test!(test_micro_m05, Some(MICRO), -1e-5, Some(MICRO));
252    impl_test!(test_micro_04, Some(MICRO), 1e-4, Some(MICRO));
253    impl_test!(test_micro_m04, Some(MICRO), -1e-4, Some(MICRO));
254
255    impl_test!(test_milli_03, Some(MILLI), 1e-3, Some(MILLI));
256    impl_test!(test_milli_02, Some(MILLI), 1e-2, Some(CENTI));
257    impl_test!(test_milli_01, Some(MILLI), 1e-1, Some(DECI));
258
259    impl_test!(test_none, None, 1e0, None);
260    impl_test!(test_deca_01, None, 1e1, Some(DECA));
261    impl_test!(test_hecto_02, None, 1e2, Some(HECTO));
262
263    impl_test!(test_kilo_03, Some(KILO), 1e3, Some(KILO));
264    impl_test!(test_kilo_04, Some(KILO), 1e4, Some(KILO));
265    impl_test!(test_kilo_05, Some(KILO), 1e5, Some(KILO));
266
267    impl_test!(test_mega_06, Some(MEGA), 1e6, Some(MEGA));
268    impl_test!(test_mega_07, Some(MEGA), 1e7, Some(MEGA));
269    impl_test!(test_mega_08, Some(MEGA), 1e8, Some(MEGA));
270
271    impl_test!(test_giga_09, Some(GIGA), 1e9, Some(GIGA));
272    impl_test!(test_giga_10, Some(GIGA), 1e10, Some(GIGA));
273    impl_test!(test_giga_11, Some(GIGA), 1e11, Some(GIGA));
274
275    impl_test!(test_tera_12, Some(TERA), 1e12, Some(TERA));
276    impl_test!(test_tera_13, Some(TERA), 1e13, Some(TERA));
277    impl_test!(test_tera_14, Some(TERA), 1e14, Some(TERA));
278
279    impl_test!(test_peta_15, Some(PETA), 1e15, Some(PETA));
280    impl_test!(test_peta_16, Some(PETA), 1e16, Some(PETA));
281    impl_test!(test_peta_17, Some(PETA), 1e17, Some(PETA));
282
283    impl_test!(test_exa_18, Some(EXA), 1e18, Some(EXA));
284    impl_test!(test_exa_19, Some(EXA), 1e19, Some(EXA));
285    impl_test!(test_exa_20, Some(EXA), 1e20, Some(EXA));
286
287    impl_test!(test_zetta_21, Some(ZETTA), 1e21, Some(ZETTA));
288    impl_test!(test_zetta_22, Some(ZETTA), 1e22, Some(ZETTA));
289    impl_test!(test_zetta_23, Some(ZETTA), 1e23, Some(ZETTA));
290
291    impl_test!(test_yotta_24, Some(YOTTA), 1e24, Some(YOTTA));
292    impl_test!(test_yotta_25, Some(YOTTA), 1e25, Some(YOTTA));
293    impl_test!(test_yotta_26, Some(YOTTA), 1e26, Some(YOTTA));
294
295    impl_test!(test_ronna_27, None, 1e27, Some(RONNA));
296    impl_test!(test_ronna_28, None, 1e28, Some(RONNA));
297    impl_test!(test_ronna_29, None, 1e29, Some(RONNA));
298    impl_test!(test_quetta_30, None, 1e30, Some(QUETTA));
299    impl_test!(test_quetta_31, None, 1e31, Some(QUETTA));
300
301    #[cfg(feature = "alloc")]
302    #[test]
303    pub fn test_format() {
304        use crate::prefixes::PrefixFormat;
305        assert_eq!("2.000k", KILO.format(&2e3));
306        assert_eq!(
307            "2.25k",
308            KILO.format_args(
309                PrefixFormat::new().with_width(2).with_precision(4),
310                &2.2501e3
311            )
312        );
313    }
314}