formualizer_eval/
format.rs1use formualizer_common::numfmt::{FormatClass, NumberFormat};
2use rustc_hash::FxHashMap;
3
4#[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#[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 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}