formualizer_eval/engine/graph/
tables.rs1use crate::SheetId;
2use crate::engine::graph::DependencyGraph;
3use crate::engine::vertex::{VertexId, VertexKind};
4use crate::reference::RangeRef;
5use formualizer_common::{ExcelError, ExcelErrorKind};
6
7#[inline]
8fn normalize_table_key(name: &str) -> String {
9 name.to_lowercase()
10}
11
12#[derive(Debug, Clone)]
14pub(crate) struct TableEntry {
15 pub(crate) name: String,
16 pub(crate) range: RangeRef,
17 pub(crate) header_row: bool,
18 pub(crate) headers: Vec<String>,
19 pub(crate) totals_row: bool,
20 pub(crate) empty_body: bool,
21 pub(crate) vertex: VertexId,
22}
23
24impl TableEntry {
25 pub fn sheet_id(&self) -> SheetId {
26 self.range.start.sheet_id
27 }
28
29 pub fn col_index(&self, header: &str) -> Option<usize> {
30 let header_key = header.to_lowercase();
31 self.headers
32 .iter()
33 .position(|h| h.to_lowercase() == header_key)
34 }
35}
36
37impl DependencyGraph {
38 pub(crate) fn table_intersects_spill(
40 &self,
41 anchor: crate::reference::CellRef,
42 end_row: u32,
43 end_col: u32,
44 ) -> bool {
45 self.tables.values().any(|t| {
46 t.sheet_id() == anchor.sheet_id
47 && t.range.start.coord.row() <= end_row
48 && anchor.coord.row() <= t.range.end.coord.row()
49 && t.range.start.coord.col() <= end_col
50 && anchor.coord.col() <= t.range.end.coord.col()
51 })
52 }
53
54 #[inline]
55 fn table_lookup_key(&self, name: &str) -> String {
56 if self.config.case_sensitive_tables {
57 name.to_string()
58 } else {
59 normalize_table_key(name)
60 }
61 }
62
63 fn canonical_table_name(&self, name: &str) -> Option<String> {
64 let key = self.table_lookup_key(name);
65 self.tables_lookup.get(&key).cloned()
66 }
67
68 pub(crate) fn resolve_table_entry(&self, name: &str) -> Option<&TableEntry> {
69 if self.config.case_sensitive_tables {
70 self.tables.get(name)
71 } else {
72 let key = self.table_lookup_key(name);
73 self.tables_lookup
74 .get(&key)
75 .and_then(|canon| self.tables.get(canon))
76 }
77 }
78
79 pub(crate) fn table_by_vertex(&self, vertex: VertexId) -> Option<&TableEntry> {
80 self.table_vertex_lookup
81 .get(&vertex)
82 .and_then(|name| self.tables.get(name))
83 }
84
85 pub fn table_names(&self) -> Vec<String> {
87 let mut names: Vec<String> = self.tables.values().map(|t| t.name.clone()).collect();
88 names.sort();
89 names
90 }
91
92 pub fn define_table(
93 &mut self,
94 name: &str,
95 range: RangeRef,
96 header_row: bool,
97 headers: Vec<String>,
98 totals_row: bool,
99 ) -> Result<(), ExcelError> {
100 if name.is_empty() {
101 return Err(ExcelError::new(ExcelErrorKind::Name)
102 .with_message("Table name cannot be empty".to_string()));
103 }
104
105 let key = self.table_lookup_key(name);
106 if let Some(existing) = self.tables_lookup.get(&key) {
107 return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
108 "Table collision under normalization: '{name}' conflicts with '{existing}'"
109 )));
110 }
111
112 let sheet_id = range.start.sheet_id;
117 let vertex = self.allocate_symbol_vertex(VertexKind::Table, sheet_id);
118
119 self.register_table_range_deps(vertex, &range);
122
123 let entry = TableEntry {
124 name: name.to_string(),
125 range,
126 header_row,
127 headers,
128 totals_row,
129 empty_body: range.start.coord.row() + 1 + u32::from(header_row)
130 > (range.end.coord.row() + 1).saturating_sub(u32::from(totals_row)),
131 vertex,
132 };
133
134 let original = name.to_string();
135 self.tables.insert(original.clone(), entry);
136 self.tables_lookup
137 .insert(self.table_lookup_key(&original), original.clone());
138 self.table_vertex_lookup.insert(vertex, original);
139 self.authority_note_symbol(Some(vertex));
140 self.bump_symbol_revision();
141 self.resolve_pending_symbol("table", name);
142 Ok(())
143 }
144
145 pub fn update_table(
146 &mut self,
147 name: &str,
148 new_range: RangeRef,
149 header_row: bool,
150 headers: Vec<String>,
151 totals_row: bool,
152 ) -> Result<(), ExcelError> {
153 let Some(canon) = self.canonical_table_name(name) else {
154 return Err(ExcelError::new(ExcelErrorKind::Name)
155 .with_message(format!("Unknown table: {name}")));
156 };
157
158 let vertex = self.tables.get(&canon).map(|t| t.vertex).ok_or_else(|| {
159 ExcelError::new(ExcelErrorKind::Name).with_message(format!("Unknown table: {name}"))
160 })?;
161
162 self.remove_dependent_edges(vertex);
164 self.register_table_range_deps(vertex, &new_range);
165
166 if let Some(existing) = self.tables.get_mut(&canon) {
167 existing.range = new_range;
168 existing.header_row = header_row;
169 existing.headers = headers;
170 existing.totals_row = totals_row;
171 existing.empty_body = new_range.start.coord.row() + 1 + u32::from(header_row)
172 > (new_range.end.coord.row() + 1).saturating_sub(u32::from(totals_row));
173 }
174
175 self.mark_dirty(vertex);
177 self.authority_note_symbol(Some(vertex));
178 self.bump_symbol_revision();
179 Ok(())
180 }
181
182 pub fn resize_table(
184 &mut self,
185 name: &str,
186 new_range: RangeRef,
187 ) -> Result<RangeRef, ExcelError> {
188 self.resize_table_body(name, new_range, false)
189 }
190
191 pub fn resize_table_body(
192 &mut self,
193 name: &str,
194 new_range: RangeRef,
195 empty_body: bool,
196 ) -> Result<RangeRef, ExcelError> {
197 let entry = self.resolve_table_entry(name).cloned().ok_or_else(|| {
198 ExcelError::new(ExcelErrorKind::Name).with_message(format!("Unknown table: {name}"))
199 })?;
200 if new_range.start != entry.range.start
201 || new_range.end.sheet_id != entry.sheet_id()
202 || new_range.end.coord.col() != entry.range.end.coord.col()
203 || new_range.end.coord.row() < entry.range.start.coord.row()
204 {
205 return Err(ExcelError::new(ExcelErrorKind::Value)
206 .with_message("Table resize must preserve its sheet, start, and columns"));
207 }
208 let old_range = entry.range;
209 if old_range == new_range && entry.empty_body == empty_body {
210 return Ok(old_range);
211 }
212 let sheet_id = entry.sheet_id();
213 self.update_table(
214 name,
215 new_range,
216 entry.header_row,
217 entry.headers,
218 entry.totals_row,
219 )?;
220 let canon = self.canonical_table_name(name).expect("validated table");
221 self.tables
222 .get_mut(&canon)
223 .expect("validated table")
224 .empty_body = empty_body;
225 self.mark_dirty_rects(&[(
227 sheet_id,
228 old_range.start.coord.row(),
229 old_range.end.coord.row().max(new_range.end.coord.row()),
230 old_range.start.coord.col(),
231 old_range.end.coord.col(),
232 )]);
233 Ok(old_range)
234 }
235
236 pub(crate) fn empty_tables_at(&self, cell: crate::reference::CellRef) -> Vec<TableEntry> {
238 self.tables
239 .values()
240 .filter(|t| {
241 t.empty_body
242 && t.sheet_id() == cell.sheet_id
243 && cell.coord.row() >= t.range.start.coord.row() + u32::from(t.header_row)
244 && cell.coord.row()
245 <= t.range
246 .end
247 .coord
248 .row()
249 .saturating_sub(u32::from(t.totals_row))
250 && cell.coord.col() >= t.range.start.coord.col()
251 && cell.coord.col() <= t.range.end.coord.col()
252 })
253 .cloned()
254 .collect()
255 }
256
257 pub fn delete_table(&mut self, name: &str) -> Result<(), ExcelError> {
258 let Some(canon) = self.canonical_table_name(name) else {
259 return Err(ExcelError::new(ExcelErrorKind::Name)
260 .with_message(format!("Unknown table: {name}")));
261 };
262
263 let Some(entry) = self.tables.remove(&canon) else {
264 return Err(ExcelError::new(ExcelErrorKind::Name)
265 .with_message(format!("Unknown table: {name}")));
266 };
267
268 self.tables_lookup.remove(&self.table_lookup_key(&canon));
269
270 let vertex = entry.vertex;
271 self.table_vertex_lookup.remove(&vertex);
272
273 self.remove_dependent_edges(vertex);
275
276 self.store.mark_deleted(vertex, true);
278 self.vertex_values.remove(&vertex);
279 self.vertex_formulas.remove(&vertex);
280 self.clear_formula_vertex_dirty(vertex);
281 self.volatile_vertices.remove(&vertex);
282 self.authority_note_symbol(Some(vertex));
283 self.bump_symbol_revision();
284
285 Ok(())
286 }
287
288 fn register_table_range_deps(&mut self, table_vertex: VertexId, range: &RangeRef) {
289 use crate::reference::SharedRangeRef;
290 use crate::reference::SharedSheetLocator;
291 use formualizer_common::AxisBound;
292
293 let sheet_loc = SharedSheetLocator::Id(range.start.sheet_id);
295 let sr = AxisBound::new(range.start.coord.row(), range.start.coord.row_abs());
296 let sc = AxisBound::new(range.start.coord.col(), range.start.coord.col_abs());
297 let er = AxisBound::new(range.end.coord.row(), range.end.coord.row_abs());
298 let ec = AxisBound::new(range.end.coord.col(), range.end.coord.col_abs());
299
300 if let Ok(r) = SharedRangeRef::from_parts(sheet_loc, Some(sr), Some(sc), Some(er), Some(ec))
301 {
302 self.add_range_dependent_edges(table_vertex, &[r.into_owned()], range.start.sheet_id);
303 }
304 }
305}