formualizer_eval/engine/
sheet_registry.rs1use std::collections::HashMap;
2
3use formualizer_common::{ExcelError, ExcelErrorKind};
4
5use crate::SheetId;
6use crate::reference::SharedSheetLocator;
7
8#[derive(Default, Debug)]
9pub struct SheetRegistry {
10 id_by_name: HashMap<String, SheetId>,
11 name_by_id: Vec<String>,
12}
13
14impl SheetRegistry {
15 pub fn new() -> Self {
16 SheetRegistry::default()
17 }
18
19 pub fn id_for(&mut self, name: &str) -> SheetId {
20 let key = name.to_lowercase();
23 if let Some(&id) = self.id_by_name.get(&key) {
24 return id;
25 }
26
27 let id = self.name_by_id.len() as SheetId;
28 self.name_by_id.push(name.to_string());
29 self.id_by_name.insert(key, id);
30 id
31 }
32
33 pub fn name(&self, id: SheetId) -> &str {
34 if (id as usize) < self.name_by_id.len() {
35 &self.name_by_id[id as usize]
36 } else {
37 ""
38 }
39 }
40
41 pub fn get_id(&self, name: &str) -> Option<SheetId> {
42 self.id_by_name.get(&name.to_lowercase()).copied()
44 }
45
46 pub fn resolve_locator(
61 &self,
62 locator: &SharedSheetLocator<'_>,
63 context_sheet: SheetId,
64 ) -> Result<SheetId, ExcelError> {
65 match locator {
66 SharedSheetLocator::Id(id) => Ok(*id),
67 SharedSheetLocator::Current => Ok(context_sheet),
68 SharedSheetLocator::Name(name) => self.get_id(name.as_ref()).ok_or_else(|| {
69 ExcelError::new(ExcelErrorKind::Ref)
70 .with_message(format!("Sheet not found: {name}"))
71 }),
72 }
73 }
74
75 pub fn active_len(&self) -> usize {
77 self.name_by_id
78 .iter()
79 .filter(|name| !name.is_empty())
80 .count()
81 }
82
83 pub fn active_position_by_id(&self, id: SheetId) -> Option<usize> {
85 let idx = id as usize;
86 if idx >= self.name_by_id.len() || self.name_by_id[idx].is_empty() {
87 return None;
88 }
89 Some(
90 self.name_by_id
91 .iter()
92 .take(idx + 1)
93 .filter(|name| !name.is_empty())
94 .count(),
95 )
96 }
97
98 pub fn active_position(&self, name: &str) -> Option<usize> {
100 self.get_id(name)
101 .and_then(|id| self.active_position_by_id(id))
102 }
103
104 pub fn active_span_len(&self, first: &str, last: &str) -> Option<usize> {
106 let a = self.active_position(first)?;
107 let b = self.active_position(last)?;
108 Some(a.abs_diff(b) + 1)
109 }
110
111 pub fn all_sheets(&self) -> Vec<(SheetId, String)> {
113 self.name_by_id
114 .iter()
115 .enumerate()
116 .filter(|(_, name)| !name.is_empty())
117 .map(|(id, name)| (id as SheetId, name.clone()))
118 .collect()
119 }
120
121 pub fn remove(&mut self, id: SheetId) -> Result<(), formualizer_common::ExcelError> {
124 use formualizer_common::{ExcelError, ExcelErrorKind};
125
126 if id as usize >= self.name_by_id.len() {
128 return Err(
129 ExcelError::new(ExcelErrorKind::Value).with_message("Sheet ID does not exist")
130 );
131 }
132
133 let name = self.name_by_id[id as usize].clone();
135 if name.is_empty() {
136 return Ok(());
138 }
139
140 self.id_by_name.remove(&name.to_lowercase());
142
143 self.name_by_id[id as usize] = String::new();
145
146 Ok(())
147 }
148
149 pub fn rename(
151 &mut self,
152 id: SheetId,
153 new_name: &str,
154 ) -> Result<(), formualizer_common::ExcelError> {
155 use formualizer_common::{ExcelError, ExcelErrorKind};
156
157 if id as usize >= self.name_by_id.len() {
159 return Err(
160 ExcelError::new(ExcelErrorKind::Value).with_message("Sheet ID does not exist")
161 );
162 }
163
164 let old_name = self.name_by_id[id as usize].clone();
166
167 if let Some(&existing_id) = self.id_by_name.get(&new_name.to_lowercase())
169 && existing_id != id
170 {
171 return Err(ExcelError::new(ExcelErrorKind::Value)
172 .with_message(format!("Sheet name '{new_name}' already exists")));
173 }
174
175 self.id_by_name.remove(&old_name.to_lowercase());
177
178 self.name_by_id[id as usize] = new_name.to_string();
180 self.id_by_name.insert(new_name.to_lowercase(), id);
181
182 Ok(())
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn sheet_names_are_case_insensitive() {
192 let mut reg = SheetRegistry::new();
193 let id = reg.id_for("CONFIG");
194 assert_eq!(reg.get_id("CONFIG"), Some(id));
196 assert_eq!(reg.get_id("Config"), Some(id));
197 assert_eq!(reg.get_id("config"), Some(id));
198 assert_eq!(reg.id_for("Config"), id);
200 assert_eq!(reg.name(id), "CONFIG");
202 }
203}