Skip to main content

uni_btic/
granularity.rs

1use crate::error::BticError;
2
3/// Granularity of a BTIC bound — the calendrical unit from which it was derived.
4///
5/// Granularity is metadata only; it does not affect comparison or ordering.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
7#[repr(u8)]
8pub enum Granularity {
9    Millisecond = 0,
10    Second = 1,
11    Minute = 2,
12    Hour = 3,
13    Day = 4,
14    Month = 5,
15    Quarter = 6,
16    Year = 7,
17    Decade = 8,
18    Century = 9,
19    Millennium = 10,
20}
21
22impl Granularity {
23    /// Construct from a 4-bit code (0x0..=0xA). Returns error for reserved codes.
24    pub fn from_code(code: u8) -> Result<Self, BticError> {
25        match code {
26            0 => Ok(Self::Millisecond),
27            1 => Ok(Self::Second),
28            2 => Ok(Self::Minute),
29            3 => Ok(Self::Hour),
30            4 => Ok(Self::Day),
31            5 => Ok(Self::Month),
32            6 => Ok(Self::Quarter),
33            7 => Ok(Self::Year),
34            8 => Ok(Self::Decade),
35            9 => Ok(Self::Century),
36            10 => Ok(Self::Millennium),
37            _ => Err(BticError::GranularityRange(code)),
38        }
39    }
40
41    /// The numeric code for this granularity (0x0..=0xA).
42    pub fn code(self) -> u8 {
43        self as u8
44    }
45
46    /// Human-readable name for this granularity.
47    pub fn name(self) -> &'static str {
48        match self {
49            Self::Millisecond => "millisecond",
50            Self::Second => "second",
51            Self::Minute => "minute",
52            Self::Hour => "hour",
53            Self::Day => "day",
54            Self::Month => "month",
55            Self::Quarter => "quarter",
56            Self::Year => "year",
57            Self::Decade => "decade",
58            Self::Century => "century",
59            Self::Millennium => "millennium",
60        }
61    }
62
63    /// Look up by name (case-insensitive).
64    pub fn from_name(s: &str) -> Option<Self> {
65        match s.to_lowercase().as_str() {
66            "millisecond" | "ms" => Some(Self::Millisecond),
67            "second" | "s" => Some(Self::Second),
68            "minute" | "min" => Some(Self::Minute),
69            "hour" | "h" => Some(Self::Hour),
70            "day" | "d" => Some(Self::Day),
71            "month" | "mon" => Some(Self::Month),
72            "quarter" | "q" => Some(Self::Quarter),
73            "year" | "y" => Some(Self::Year),
74            "decade" => Some(Self::Decade),
75            "century" => Some(Self::Century),
76            "millennium" => Some(Self::Millennium),
77            _ => None,
78        }
79    }
80
81    /// Returns the finer (more precise) of two granularities (lower code = finer).
82    pub fn finer(self, other: Self) -> Self {
83        std::cmp::min(self, other)
84    }
85}
86
87impl std::fmt::Display for Granularity {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.write_str(self.name())
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn roundtrip_all_codes() {
99        for code in 0..=10u8 {
100            let g = Granularity::from_code(code).unwrap();
101            assert_eq!(g.code(), code);
102        }
103    }
104
105    #[test]
106    fn reserved_codes_rejected() {
107        for code in 11..=15u8 {
108            assert!(Granularity::from_code(code).is_err());
109        }
110    }
111
112    #[test]
113    fn name_roundtrip() {
114        for code in 0..=10u8 {
115            let g = Granularity::from_code(code).unwrap();
116            let g2 = Granularity::from_name(g.name()).unwrap();
117            assert_eq!(g, g2);
118        }
119    }
120
121    #[test]
122    fn finer_picks_lower_code() {
123        assert_eq!(Granularity::Day.finer(Granularity::Year), Granularity::Day);
124        assert_eq!(
125            Granularity::Year.finer(Granularity::Month),
126            Granularity::Month
127        );
128        assert_eq!(
129            Granularity::Millisecond.finer(Granularity::Millennium),
130            Granularity::Millisecond
131        );
132    }
133}