Skip to main content

formualizer_eval/
reference.rs

1//! Cell, coordinate, and range reference utilities for a spreadsheet engine.
2//!
3//! ## Design goals
4//! * **Compact**: small, `Copy`‑able types (12–16 bytes) that can be placed in large
5//!   dependency graphs without GC/heap pressure.
6//! * **Excel‑compatible semantics**: four anchoring modes (`A1`, `$A1`, `A$1`, `$A$1`)
7//!   plus optional sheet scoping.
8//! * **Utility helpers**: rebasing, offsetting, (de)serialising, and pretty `Display`.
9//!
10//! ----
11//!
12//! ```text
13//! ┌──────────┐    1) Parser/loader creates         ┌─────────────┐
14//! │  Coord   │────┐                                 │   CellRef   │
15//! └──────────┘    └──────┐      2) Linker inserts ─▶└─────────────┘
16//!  row, col, flags        │      SheetId + range
17//!                         ▼
18//!                ┌────────────────┐   (RangeRef = 2×CellRef)
19//!                │ Evaluation IR  │  (row/col absolute, flags dropped)
20//!                └────────────────┘
21//! ```
22
23use core::fmt;
24
25use crate::engine::sheet_registry::SheetRegistry; // `no_std`‑friendly; swap for `std::fmt` if you prefer
26use formualizer_common::{
27    ExcelError, ExcelErrorKind, RelativeCoord, SheetCellRef as CommonSheetCellRef,
28    SheetId as CommonSheetId, SheetLocator as CommonSheetLocator,
29    SheetRangeRef as CommonSheetRangeRef, SheetRef as CommonSheetRef,
30};
31use formualizer_parse::parser::ReferenceType;
32
33//------------------------------------------------------------------------------
34// Shared ref aliases (Phase 3.2 staging)
35//------------------------------------------------------------------------------
36
37pub type SharedSheetId = CommonSheetId;
38pub type SharedSheetLocator<'a> = CommonSheetLocator<'a>;
39pub type SharedCellRef<'a> = CommonSheetCellRef<'a>;
40pub type SharedRangeRef<'a> = CommonSheetRangeRef<'a>;
41pub type SharedRef<'a> = CommonSheetRef<'a>;
42
43//------------------------------------------------------------------------------
44// Coord
45//------------------------------------------------------------------------------
46
47/// One 2‑D grid coordinate (row, column) **plus** absolute/relative flags.
48///
49/// Internally delegates to `RelativeCoord` from `formualizer-common`, adding the
50/// historical API surface used throughout the evaluator.
51#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
52pub struct Coord(RelativeCoord);
53
54impl Coord {
55    #[inline]
56    pub fn new(row: u32, col: u32, row_abs: bool, col_abs: bool) -> Self {
57        Self(RelativeCoord::new(row, col, row_abs, col_abs))
58    }
59
60    #[inline]
61    pub fn from_excel(row: u32, col: u32, row_abs: bool, col_abs: bool) -> Self {
62        let row0 = row.saturating_sub(1);
63        let col0 = col.saturating_sub(1);
64        Self(RelativeCoord::new(row0, col0, row_abs, col_abs))
65    }
66
67    #[inline]
68    pub fn row(self) -> u32 {
69        self.0.row()
70    }
71
72    #[inline]
73    pub fn col(self) -> u32 {
74        self.0.col()
75    }
76
77    #[inline]
78    pub fn row_abs(self) -> bool {
79        self.0.row_abs()
80    }
81
82    #[inline]
83    pub fn col_abs(self) -> bool {
84        self.0.col_abs()
85    }
86
87    #[inline]
88    pub fn with_row_abs(self, abs: bool) -> Self {
89        Self(self.0.with_row_abs(abs))
90    }
91
92    #[inline]
93    pub fn with_col_abs(self, abs: bool) -> Self {
94        Self(self.0.with_col_abs(abs))
95    }
96
97    #[inline]
98    pub fn offset(self, drow: i32, dcol: i32) -> Self {
99        Self(self.0.offset(drow, dcol))
100    }
101
102    #[inline]
103    pub fn rebase(self, origin: Coord, target: Coord) -> Self {
104        Self(self.0.rebase(origin.0, target.0))
105    }
106
107    #[inline]
108    pub fn into_inner(self) -> RelativeCoord {
109        self.0
110    }
111
112    pub fn col_to_letters(col: u32) -> String {
113        RelativeCoord::col_to_letters(col)
114    }
115
116    pub fn letters_to_col(s: &str) -> Option<u32> {
117        RelativeCoord::letters_to_col(s)
118    }
119}
120
121type SheetBounds = (Option<String>, (u32, u32, u32, u32));
122
123/// Combine two references with the range operator ':'
124/// Supports combining Cell:Cell, Cell:Range (and Range:Cell), and Range:Range on the same sheet.
125/// Returns #REF! for cross-sheet combinations or incompatible shapes.
126pub fn combine_references(
127    a: &ReferenceType,
128    b: &ReferenceType,
129) -> Result<ReferenceType, ExcelError> {
130    // Extract sheet and bounds as (sheet, (sr, sc, er, ec))
131    fn to_bounds(r: &ReferenceType) -> Option<SheetBounds> {
132        match r {
133            ReferenceType::Cell {
134                sheet, row, col, ..
135            } => Some((sheet.clone(), (*row, *col, *row, *col))),
136            ReferenceType::Range {
137                sheet,
138                start_row,
139                start_col,
140                end_row,
141                end_col,
142                ..
143            } => {
144                let (sr, sc, er, ec) = match (start_row, start_col, end_row, end_col) {
145                    (Some(sr), Some(sc), Some(er), Some(ec)) => (*sr, *sc, *er, *ec),
146                    _ => return None,
147                };
148                Some((sheet.clone(), (sr, sc, er, ec)))
149            }
150            _ => None,
151        }
152    }
153
154    let (sheet_a, (a_sr, a_sc, a_er, a_ec)) = to_bounds(a).ok_or_else(|| {
155        ExcelError::new(ExcelErrorKind::Ref).with_message("Unsupported reference for ':'")
156    })?;
157    let (sheet_b, (b_sr, b_sc, b_er, b_ec)) = to_bounds(b).ok_or_else(|| {
158        ExcelError::new(ExcelErrorKind::Ref).with_message("Unsupported reference for ':'")
159    })?;
160
161    // Sheets must match (both None or equal Some)
162    if sheet_a != sheet_b {
163        return Err(ExcelError::new(ExcelErrorKind::Ref)
164            .with_message("Cannot combine references across sheets"));
165    }
166
167    let sr = a_sr.min(b_sr);
168    let sc = a_sc.min(b_sc);
169    let er = a_er.max(b_er);
170    let ec = a_ec.max(b_ec);
171
172    Ok(ReferenceType::Range {
173        sheet: sheet_a,
174        start_row: Some(sr),
175        start_col: Some(sc),
176        end_row: Some(er),
177        end_col: Some(ec),
178        start_row_abs: false,
179        start_col_abs: false,
180        end_row_abs: false,
181        end_col_abs: false,
182    })
183}
184
185impl fmt::Display for Coord {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        if self.col_abs() {
188            write!(f, "$")?;
189        }
190        write!(f, "{}", Self::col_to_letters(self.col()))?;
191        if self.row_abs() {
192            write!(f, "$")?;
193        }
194        // rows are 1‑based in A1 notation
195        write!(f, "{}", self.row() + 1)
196    }
197}
198
199//------------------------------------------------------------------------------
200// CellRef
201//------------------------------------------------------------------------------
202
203/// Sheet identifier inside a workbook.
204///
205/// Sheet ids are assigned by the engine/registry and have no sentinel values.
206pub type SheetId = u16; // 65,535 sheets should be enough for anyone.
207
208#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
209pub struct CellRef {
210    pub sheet_id: SheetId,
211    pub coord: Coord,
212}
213
214impl CellRef {
215    #[inline]
216    pub const fn new(sheet_id: SheetId, coord: Coord) -> Self {
217        Self { sheet_id, coord }
218    }
219
220    #[inline]
221    pub fn new_absolute(sheet_id: SheetId, row: u32, col: u32) -> Self {
222        Self {
223            sheet_id,
224            coord: Coord::new(row, col, true, true),
225        }
226    }
227
228    /// Rebase using underlying `Coord` logic.
229    #[inline]
230    pub fn rebase(self, origin: Coord, target: Coord) -> Self {
231        Self {
232            sheet_id: self.sheet_id,
233            coord: self.coord.rebase(origin, target),
234        }
235    }
236
237    #[inline]
238    pub fn sheet_name<'a>(&self, sheet_reg: &'a SheetRegistry) -> &'a str {
239        sheet_reg.name(self.sheet_id)
240    }
241
242    #[inline]
243    pub fn to_shared(self) -> SharedCellRef<'static> {
244        SharedCellRef::new(
245            SharedSheetLocator::Id(self.sheet_id),
246            self.coord.into_inner(),
247        )
248    }
249
250    pub fn try_from_shared(cell: SharedCellRef<'_>) -> Result<Self, ExcelError> {
251        let owned = cell.into_owned();
252        let sheet_id = match owned.sheet {
253            SharedSheetLocator::Id(id) => id,
254            _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
255        };
256        Ok(Self::new(sheet_id, Coord(owned.coord)))
257    }
258}
259
260impl fmt::Display for CellRef {
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        // Always include the sheet id; there is no longer a "current sheet" sentinel.
263        write!(f, "Sheet{}!", self.sheet_id)?;
264        write!(f, "{}", self.coord)
265    }
266}
267
268//------------------------------------------------------------------------------
269// RangeRef (half‑open range helper)
270//------------------------------------------------------------------------------
271
272#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
273pub struct RangeRef {
274    pub start: CellRef,
275    pub end: CellRef, // inclusive like Excel: A1:B5 covers both corners
276}
277
278impl RangeRef {
279    #[inline]
280    pub const fn new(start: CellRef, end: CellRef) -> Self {
281        Self { start, end }
282    }
283
284    pub fn try_to_shared(self) -> Result<SharedRangeRef<'static>, ExcelError> {
285        if self.start.sheet_id != self.end.sheet_id {
286            return Err(ExcelError::new(ExcelErrorKind::Ref));
287        }
288        let sheet = SharedSheetLocator::Id(self.start.sheet_id);
289        let sr =
290            formualizer_common::AxisBound::new(self.start.coord.row(), self.start.coord.row_abs());
291        let sc =
292            formualizer_common::AxisBound::new(self.start.coord.col(), self.start.coord.col_abs());
293        let er = formualizer_common::AxisBound::new(self.end.coord.row(), self.end.coord.row_abs());
294        let ec = formualizer_common::AxisBound::new(self.end.coord.col(), self.end.coord.col_abs());
295        SharedRangeRef::from_parts(sheet, Some(sr), Some(sc), Some(er), Some(ec))
296            .map_err(|_| ExcelError::new(ExcelErrorKind::Ref))
297    }
298
299    pub fn try_from_shared(range: SharedRangeRef<'_>) -> Result<Self, ExcelError> {
300        let owned = range.into_owned();
301        let sheet_id = match owned.sheet {
302            SharedSheetLocator::Id(id) => id,
303            _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
304        };
305        let (sr, sc, er, ec) = match (
306            owned.start_row,
307            owned.start_col,
308            owned.end_row,
309            owned.end_col,
310        ) {
311            (Some(sr), Some(sc), Some(er), Some(ec)) => (sr, sc, er, ec),
312            _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
313        };
314        let start = CellRef::new(sheet_id, Coord::new(sr.index, sc.index, sr.abs, sc.abs));
315        let end = CellRef::new(sheet_id, Coord::new(er.index, ec.index, er.abs, ec.abs));
316        Ok(Self::new(start, end))
317    }
318}
319
320impl fmt::Display for RangeRef {
321    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322        if self.start.sheet_id == self.end.sheet_id {
323            // Single sheet: prefix once
324            write!(f, "{}:{}", self.start, self.end.coord)
325        } else {
326            // Different sheets: print fully.
327            write!(f, "{}:{}", self.start, self.end)
328        }
329    }
330}
331
332//------------------------------------------------------------------------------
333// Tests
334//------------------------------------------------------------------------------
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_display_coord() {
342        let c = Coord::new(0, 0, false, false);
343        assert_eq!(c.to_string(), "A1");
344        let c = Coord::new(7, 27, true, true); // row 8, col 28 == AB
345        assert_eq!(c.to_string(), "$AB$8");
346    }
347
348    #[test]
349    fn test_rebase() {
350        let origin = Coord::new(0, 0, false, false);
351        let target = Coord::new(1, 1, false, false);
352        let formula_coord = Coord::new(2, 0, false, true); // A3 with absolute col
353        let rebased = formula_coord.rebase(origin, target);
354        // Should move down 1 row, col stays because absolute
355        assert_eq!(rebased, Coord::new(3, 0, false, true));
356    }
357
358    #[test]
359    fn test_range_display() {
360        let a1 = CellRef::new(0, Coord::new(0, 0, false, false));
361        let b2 = CellRef::new(0, Coord::new(1, 1, false, false));
362        let r = RangeRef::new(a1, b2);
363        assert_eq!(r.to_string(), "Sheet0!A1:B2");
364    }
365}