Skip to main content

formualizer_eval/engine/
sheet_registry.rs

1use std::collections::HashMap;
2
3use crate::SheetId;
4
5#[derive(Default, Debug)]
6pub struct SheetRegistry {
7    id_by_name: HashMap<String, SheetId>,
8    name_by_id: Vec<String>,
9}
10
11impl SheetRegistry {
12    pub fn new() -> Self {
13        SheetRegistry::default()
14    }
15
16    pub fn id_for(&mut self, name: &str) -> SheetId {
17        if let Some(&id) = self.id_by_name.get(name) {
18            return id;
19        }
20
21        let id = self.name_by_id.len() as SheetId;
22        self.name_by_id.push(name.to_string());
23        self.id_by_name.insert(name.to_string(), id);
24        id
25    }
26
27    pub fn name(&self, id: SheetId) -> &str {
28        if (id as usize) < self.name_by_id.len() {
29            &self.name_by_id[id as usize]
30        } else {
31            ""
32        }
33    }
34
35    pub fn get_id(&self, name: &str) -> Option<SheetId> {
36        self.id_by_name.get(name).copied()
37    }
38
39    /// Count active sheets without cloning sheet names.
40    pub fn active_len(&self) -> usize {
41        self.name_by_id
42            .iter()
43            .filter(|name| !name.is_empty())
44            .count()
45    }
46
47    /// Excel-style 1-based active sheet position for a sheet id.
48    pub fn active_position_by_id(&self, id: SheetId) -> Option<usize> {
49        let idx = id as usize;
50        if idx >= self.name_by_id.len() || self.name_by_id[idx].is_empty() {
51            return None;
52        }
53        Some(
54            self.name_by_id
55                .iter()
56                .take(idx + 1)
57                .filter(|name| !name.is_empty())
58                .count(),
59        )
60    }
61
62    /// Excel-style 1-based active sheet position for a sheet name.
63    pub fn active_position(&self, name: &str) -> Option<usize> {
64        self.get_id(name)
65            .and_then(|id| self.active_position_by_id(id))
66    }
67
68    /// Inclusive count of active sheets between two sheet names.
69    pub fn active_span_len(&self, first: &str, last: &str) -> Option<usize> {
70        let a = self.active_position(first)?;
71        let b = self.active_position(last)?;
72        Some(a.abs_diff(b) + 1)
73    }
74
75    /// Get all sheet IDs and names (excluding removed sheets)
76    pub fn all_sheets(&self) -> Vec<(SheetId, String)> {
77        self.name_by_id
78            .iter()
79            .enumerate()
80            .filter(|(_, name)| !name.is_empty())
81            .map(|(id, name)| (id as SheetId, name.clone()))
82            .collect()
83    }
84
85    /// Remove a sheet from the registry
86    /// Note: This doesn't actually free the ID, it just marks it as removed
87    pub fn remove(&mut self, id: SheetId) -> Result<(), formualizer_common::ExcelError> {
88        use formualizer_common::{ExcelError, ExcelErrorKind};
89
90        // Check if the ID exists
91        if id as usize >= self.name_by_id.len() {
92            return Err(
93                ExcelError::new(ExcelErrorKind::Value).with_message("Sheet ID does not exist")
94            );
95        }
96
97        // Get the name to remove from id_by_name
98        let name = self.name_by_id[id as usize].clone();
99        if name.is_empty() {
100            // Already removed
101            return Ok(());
102        }
103
104        // Remove from id_by_name mapping
105        self.id_by_name.remove(&name);
106
107        // Mark as removed in name_by_id (we can't actually remove it to preserve IDs)
108        self.name_by_id[id as usize] = String::new();
109
110        Ok(())
111    }
112
113    /// Rename a sheet
114    pub fn rename(
115        &mut self,
116        id: SheetId,
117        new_name: &str,
118    ) -> Result<(), formualizer_common::ExcelError> {
119        use formualizer_common::{ExcelError, ExcelErrorKind};
120
121        // Check if the ID exists
122        if id as usize >= self.name_by_id.len() {
123            return Err(
124                ExcelError::new(ExcelErrorKind::Value).with_message("Sheet ID does not exist")
125            );
126        }
127
128        // Get the old name
129        let old_name = self.name_by_id[id as usize].clone();
130
131        // Check if new name is already taken by another sheet
132        if let Some(&existing_id) = self.id_by_name.get(new_name)
133            && existing_id != id
134        {
135            return Err(ExcelError::new(ExcelErrorKind::Value)
136                .with_message(format!("Sheet name '{new_name}' already exists")));
137        }
138
139        // Remove old name mapping
140        self.id_by_name.remove(&old_name);
141
142        // Update to new name
143        self.name_by_id[id as usize] = new_name.to_string();
144        self.id_by_name.insert(new_name.to_string(), id);
145
146        Ok(())
147    }
148}