Skip to main content

formualizer_workbook/
resolver.rs

1use crate::traits::SpreadsheetReader;
2use crate::traits::{DefinedNameDefinition, DefinedNameScope};
3use formualizer_common::{
4    LiteralValue,
5    error::{ExcelError, ExcelErrorKind},
6};
7use formualizer_eval::function::Function;
8use formualizer_eval::traits::{
9    FunctionProvider, InMemoryRange, NamedRangeResolver, Range, RangeResolver, ReferenceResolver,
10    Resolver, Table, TableResolver,
11};
12use formualizer_parse::parser::TableReference;
13use parking_lot::RwLock;
14use std::sync::Arc;
15
16/// Minimal resolver for ranges/tables (NOT cells - Engine handles cells from graph)
17pub struct IoResolver<B: SpreadsheetReader> {
18    backend: RwLock<B>,
19}
20
21impl<B: SpreadsheetReader> IoResolver<B> {
22    pub fn new(backend: B) -> Self {
23        Self {
24            backend: RwLock::new(backend),
25        }
26    }
27}
28
29// IoResolver does NOT implement ReferenceResolver (Engine handles cells from graph)
30
31impl<B: SpreadsheetReader> RangeResolver for IoResolver<B> {
32    fn resolve_range_reference(
33        &self,
34        sheet: Option<&str>,
35        sr: Option<u32>,
36        sc: Option<u32>,
37        er: Option<u32>,
38        ec: Option<u32>,
39    ) -> Result<Box<dyn Range>, ExcelError> {
40        let sheet_name = sheet.ok_or_else(|| {
41            ExcelError::new(ExcelErrorKind::Ref).with_message("Missing sheet name")
42        })?;
43        let (sr, sc, er, ec) = normalize_range(sr, sc, er, ec)?;
44
45        // Read from backend with interior mutability
46        let mut guard = self.backend.write();
47        let map = guard
48            .read_range(sheet_name, (sr, sc), (er, ec))
49            .map_err(|e| ExcelError::new(ExcelErrorKind::NImpl).with_message(e.to_string()))?;
50
51        let height = (er - sr + 1) as usize;
52        let width = (ec - sc + 1) as usize;
53        let mut rows = vec![vec![LiteralValue::Empty; width]; height];
54        for ((r, c), cell) in map.into_iter() {
55            let rr = (r - sr) as usize;
56            let cc = (c - sc) as usize;
57            if let Some(v) = cell.value {
58                rows[rr][cc] = v;
59            } else {
60                rows[rr][cc] = LiteralValue::Empty;
61            }
62        }
63        Ok(Box::new(InMemoryRange::new(rows)))
64    }
65}
66
67impl<B: SpreadsheetReader> NamedRangeResolver for IoResolver<B> {
68    fn resolve_named_range_reference(
69        &self,
70        _name: &str,
71    ) -> Result<Vec<Vec<LiteralValue>>, ExcelError> {
72        // Check if backend supports named ranges
73        if !self.backend.read().capabilities().named_ranges {
74            return Err(ExcelError::new(ExcelErrorKind::Name)
75                .with_message("Backend doesn't support named ranges"));
76        }
77
78        let name = _name;
79
80        let mut guard = self.backend.write();
81        let defined = guard
82            .defined_names()
83            .map_err(|e| ExcelError::new(ExcelErrorKind::NImpl).with_message(e.to_string()))?;
84
85        // Collect candidates (workbook + any sheet scopes). Since this resolver doesn't have
86        // a current-sheet context, sheet-scoped names are only resolvable when unique.
87        let mut matches = defined
88            .into_iter()
89            .filter(|dn| dn.name == name)
90            .collect::<Vec<_>>();
91
92        if matches.is_empty() {
93            return Err(ExcelError::new(ExcelErrorKind::Name)
94                .with_message(format!("Undefined name: {name}")));
95        }
96
97        // Prefer workbook-scoped if present and unique; otherwise require exactly one match.
98        let chosen = if let Some(wb) = matches
99            .iter()
100            .position(|dn| matches!(dn.scope, DefinedNameScope::Workbook))
101        {
102            // If both workbook + sheet scoped exist, this is ambiguous without a sheet context.
103            if matches.len() > 1 {
104                return Err(ExcelError::new(ExcelErrorKind::Name)
105                    .with_message(format!("Ambiguous name without sheet context: {name}")));
106            }
107            matches.swap_remove(wb)
108        } else {
109            if matches.len() != 1 {
110                return Err(ExcelError::new(ExcelErrorKind::Name)
111                    .with_message(format!("Ambiguous name without sheet context: {name}")));
112            }
113            matches.pop().unwrap()
114        };
115
116        match chosen.definition {
117            DefinedNameDefinition::Range { address } => {
118                // Delegate to RangeResolver reading from backend.
119                let range = guard
120                    .read_range(
121                        &address.sheet,
122                        (address.start_row, address.start_col),
123                        (address.end_row, address.end_col),
124                    )
125                    .map_err(|e| {
126                        ExcelError::new(ExcelErrorKind::NImpl).with_message(e.to_string())
127                    })?;
128
129                let h = (address.end_row - address.start_row + 1) as usize;
130                let w = (address.end_col - address.start_col + 1) as usize;
131                let mut rows = vec![vec![LiteralValue::Empty; w]; h];
132                for ((r, c), cell) in range.into_iter() {
133                    let rr = (r - address.start_row) as usize;
134                    let cc = (c - address.start_col) as usize;
135                    rows[rr][cc] = cell.value.unwrap_or(LiteralValue::Empty);
136                }
137                Ok(rows)
138            }
139            DefinedNameDefinition::Literal { value } => Ok(vec![vec![value]]),
140        }
141    }
142}
143
144impl<B: SpreadsheetReader> TableResolver for IoResolver<B> {
145    fn resolve_table_reference(
146        &self,
147        _tref: &TableReference,
148    ) -> Result<Box<dyn Table>, ExcelError> {
149        // Check if backend supports tables
150        if !self.backend.read().capabilities().tables {
151            return Err(ExcelError::new(ExcelErrorKind::NImpl)
152                .with_message("Backend doesn't support tables"));
153        }
154
155        let tref = _tref;
156
157        // Locate the table definition (tables are workbook-global; scan sheets).
158        let mut guard = self.backend.write();
159        let sheets = guard
160            .sheet_names()
161            .map_err(|e| ExcelError::new(ExcelErrorKind::NImpl).with_message(e.to_string()))?;
162
163        let mut found: Option<(String, crate::traits::TableDefinition)> = None;
164        for s in sheets {
165            let sd = guard
166                .read_sheet(&s)
167                .map_err(|e| ExcelError::new(ExcelErrorKind::NImpl).with_message(e.to_string()))?;
168            if let Some(td) = sd.tables.into_iter().find(|t| t.name == tref.name) {
169                found = Some((s, td));
170                break;
171            }
172        }
173
174        let (sheet, table) = found.ok_or_else(|| {
175            ExcelError::new(ExcelErrorKind::Name)
176                .with_message(format!("Undefined table: {}", tref.name))
177        })?;
178
179        let (sr, sc, er, ec) = table.range;
180        let map = guard
181            .read_range(&sheet, (sr, sc), (er, ec))
182            .map_err(|e| ExcelError::new(ExcelErrorKind::NImpl).with_message(e.to_string()))?;
183        let height = (er - sr + 1) as usize;
184        let width = (ec - sc + 1) as usize;
185        let mut rows = vec![vec![LiteralValue::Empty; width]; height];
186        for ((r, c), cell) in map.into_iter() {
187            let rr = (r - sr) as usize;
188            let cc = (c - sc) as usize;
189            rows[rr][cc] = cell.value.unwrap_or(LiteralValue::Empty);
190        }
191
192        Ok(Box::new(BackendTable {
193            headers: table.headers,
194            header_row: table.header_row,
195            totals_row: table.totals_row,
196            full: rows,
197        }))
198    }
199}
200
201#[derive(Clone, Debug)]
202struct BackendTable {
203    headers: Vec<String>,
204    header_row: bool,
205    totals_row: bool,
206    full: Vec<Vec<LiteralValue>>, // includes header/totals rows as present
207}
208
209impl BackendTable {
210    fn col_index(&self, header: &str) -> Option<usize> {
211        self.headers
212            .iter()
213            .position(|h| h.eq_ignore_ascii_case(header))
214    }
215
216    fn body_bounds(&self) -> (usize, usize) {
217        let h = self.full.len();
218        let start = if self.header_row { 1 } else { 0 };
219        let end_exclusive = if self.totals_row && h > 0 {
220            h.saturating_sub(1)
221        } else {
222            h
223        };
224        (start.min(h), end_exclusive.min(h))
225    }
226}
227
228impl Table for BackendTable {
229    fn get_cell(&self, r: usize, c: &str) -> Result<LiteralValue, ExcelError> {
230        let idx = self.col_index(c).ok_or_else(|| {
231            ExcelError::new(ExcelErrorKind::Ref)
232                .with_message("Column refers to unknown table column".to_string())
233        })?;
234        let (start, end_excl) = self.body_bounds();
235        let body_h = end_excl.saturating_sub(start);
236        if r >= body_h {
237            return Err(ExcelError::new(ExcelErrorKind::Ref)
238                .with_message("Row out of range for table data".to_string()));
239        }
240        Ok(self.full[start + r]
241            .get(idx)
242            .cloned()
243            .unwrap_or(LiteralValue::Empty))
244    }
245
246    fn get_column(&self, c: &str) -> Result<Box<dyn Range>, ExcelError> {
247        let idx = self.col_index(c).ok_or_else(|| {
248            ExcelError::new(ExcelErrorKind::Ref)
249                .with_message("Column refers to unknown table column".to_string())
250        })?;
251        let (start, end_excl) = self.body_bounds();
252        let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(end_excl.saturating_sub(start));
253        for r in start..end_excl {
254            let v = self.full[r]
255                .get(idx)
256                .cloned()
257                .unwrap_or(LiteralValue::Empty);
258            out.push(vec![v]);
259        }
260        Ok(Box::new(InMemoryRange::new(out)))
261    }
262
263    fn columns(&self) -> Vec<String> {
264        self.headers.clone()
265    }
266
267    fn data_height(&self) -> usize {
268        let (start, end_excl) = self.body_bounds();
269        end_excl.saturating_sub(start)
270    }
271
272    fn has_headers(&self) -> bool {
273        self.header_row
274    }
275
276    fn has_totals(&self) -> bool {
277        self.totals_row
278    }
279
280    fn headers_row(&self) -> Option<Box<dyn Range>> {
281        if !self.header_row || self.full.is_empty() {
282            return None;
283        }
284        Some(Box::new(InMemoryRange::new(vec![self.full[0].clone()])))
285    }
286
287    fn totals_row(&self) -> Option<Box<dyn Range>> {
288        if !self.totals_row || self.full.is_empty() {
289            return None;
290        }
291        Some(Box::new(InMemoryRange::new(vec![
292            self.full[self.full.len() - 1].clone(),
293        ])))
294    }
295
296    fn data_body(&self) -> Option<Box<dyn Range>> {
297        let (start, end_excl) = self.body_bounds();
298        if start >= end_excl {
299            return Some(Box::new(InMemoryRange::new(vec![])));
300        }
301        Some(Box::new(InMemoryRange::new(
302            self.full[start..end_excl].to_vec(),
303        )))
304    }
305
306    fn clone_box(&self) -> Box<dyn Table> {
307        Box::new(self.clone())
308    }
309}
310
311impl<B: SpreadsheetReader> FunctionProvider for IoResolver<B> {
312    fn get_function(&self, ns: &str, name: &str) -> Option<Arc<dyn Function>> {
313        // Delegate to global registry
314        formualizer_eval::function_registry::get(ns, name)
315    }
316}
317
318// IoResolver needs to implement ReferenceResolver for cells
319// Even though Engine handles cells from graph, trait requires it
320impl<B: SpreadsheetReader> ReferenceResolver for IoResolver<B> {
321    fn resolve_cell_reference(
322        &self,
323        _sheet: Option<&str>,
324        _row: u32,
325        _col: u32,
326    ) -> Result<LiteralValue, ExcelError> {
327        // IoResolver doesn't handle cells - Engine reads from graph
328        // This is just to satisfy the trait requirement
329        Err(ExcelError::new(ExcelErrorKind::Ref)
330            .with_message("IoResolver doesn't handle cell references"))
331    }
332}
333
334impl<B: SpreadsheetReader> Resolver for IoResolver<B> {}
335
336fn normalize_range(
337    sr: Option<u32>,
338    sc: Option<u32>,
339    er: Option<u32>,
340    ec: Option<u32>,
341) -> Result<(u32, u32, u32, u32), ExcelError> {
342    // Default to single cell if not specified
343    let sr = sr.unwrap_or(1);
344    let sc = sc.unwrap_or(1);
345    let er = er.unwrap_or(sr);
346    let ec = ec.unwrap_or(sc);
347
348    // Validate range
349    if sr > er || sc > ec {
350        return Err(ExcelError::new(ExcelErrorKind::Ref).with_message("Invalid range: start > end"));
351    }
352
353    Ok((sr, sc, er, ec))
354}