Skip to main content

formualizer_eval/
format.rs

1use formualizer_common::numfmt::{FormatClass, NumberFormat};
2use rustc_hash::FxHashMap;
3
4/// Workbook-local interned number-format identifier.
5#[repr(transparent)]
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct FormatId(pub u16);
8
9impl FormatId {
10    pub const GENERAL: Self = Self(0);
11    pub const DATE: Self = Self(14);
12    pub const TIME: Self = Self(21);
13    pub const DATETIME: Self = Self(22);
14    pub const DURATION: Self = Self(46);
15}
16
17/// Workbook-local registry of classified number-format codes.
18#[derive(Clone, Debug)]
19pub struct FormatRegistry {
20    formats: Vec<Option<NumberFormat>>,
21    by_code: FxHashMap<Box<str>, FormatId>,
22}
23
24impl Default for FormatRegistry {
25    fn default() -> Self {
26        let mut formats = Vec::with_capacity(50);
27        let mut by_code = FxHashMap::default();
28        for raw_id in 0..=49u16 {
29            let format = NumberFormat::builtin(raw_id).cloned();
30            if let Some(format) = &format {
31                by_code.insert(format.code().into(), FormatId(raw_id));
32            }
33            formats.push(format);
34        }
35        Self { formats, by_code }
36    }
37}
38
39impl FormatRegistry {
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Intern a format code.
45    ///
46    /// The u16 id space is saturated deliberately: once all ids are occupied,
47    /// the registry logs the loss and returns General without inserting the code.
48    pub fn intern(&mut self, code: &str) -> FormatId {
49        let parsed = NumberFormat::parse(code);
50        if let Some(id) = self.by_code.get(parsed.code()) {
51            return *id;
52        }
53        let Ok(raw_id) = u16::try_from(self.formats.len()) else {
54            eprintln!(
55                "number-format registry exhausted at {} entries; saturating `{}` to General",
56                self.formats.len(),
57                parsed.code()
58            );
59            return FormatId::GENERAL;
60        };
61        let id = FormatId(raw_id);
62        self.by_code.insert(parsed.code().into(), id);
63        self.formats.push(Some(parsed));
64        id
65    }
66
67    pub fn get(&self, id: FormatId) -> Option<&NumberFormat> {
68        self.formats.get(id.0 as usize).and_then(Option::as_ref)
69    }
70
71    pub fn class(&self, id: FormatId) -> Option<&FormatClass> {
72        self.get(id).map(NumberFormat::class)
73    }
74
75    pub fn code(&self, id: FormatId) -> Option<&str> {
76        self.get(id).map(NumberFormat::code)
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn builtins_keep_stable_ids_and_custom_codes_are_interned() {
86        let mut registry = FormatRegistry::new();
87        assert_eq!(registry.class(FormatId::DATE), Some(&FormatClass::Date));
88        assert_eq!(registry.intern("m/d/yy"), FormatId::DATE);
89        let custom = registry.intern("yyyy-mm-dd");
90        assert_eq!(custom, registry.intern("yyyy-mm-dd"));
91        assert_eq!(registry.class(custom), Some(&FormatClass::Date));
92        assert!(custom.0 >= 50);
93    }
94
95    #[test]
96    fn exhausted_registry_saturates_explicitly_without_inserting() {
97        let mut registry = FormatRegistry::new();
98        registry.formats.resize(usize::from(u16::MAX) + 1, None);
99        let before = registry.formats.len();
100        assert_eq!(registry.intern("0.000000custom"), FormatId::GENERAL);
101        assert_eq!(registry.formats.len(), before);
102        assert!(!registry.by_code.contains_key("0.000000custom"));
103    }
104}