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 { sheet, row, col } => {
134                Some((sheet.clone(), (*row, *col, *row, *col)))
135            }
136            ReferenceType::Range {
137                sheet,
138                start_row,
139                start_col,
140                end_row,
141                end_col,
142            } => {
143                let (sr, sc, er, ec) = match (start_row, start_col, end_row, end_col) {
144                    (Some(sr), Some(sc), Some(er), Some(ec)) => (*sr, *sc, *er, *ec),
145                    _ => return None,
146                };
147                Some((sheet.clone(), (sr, sc, er, ec)))
148            }
149            _ => None,
150        }
151    }
152
153    let (sheet_a, (a_sr, a_sc, a_er, a_ec)) = to_bounds(a).ok_or_else(|| {
154        ExcelError::new(ExcelErrorKind::Ref).with_message("Unsupported reference for ':'")
155    })?;
156    let (sheet_b, (b_sr, b_sc, b_er, b_ec)) = to_bounds(b).ok_or_else(|| {
157        ExcelError::new(ExcelErrorKind::Ref).with_message("Unsupported reference for ':'")
158    })?;
159
160    // Sheets must match (both None or equal Some)
161    if sheet_a != sheet_b {
162        return Err(ExcelError::new(ExcelErrorKind::Ref)
163            .with_message("Cannot combine references across sheets"));
164    }
165
166    let sr = a_sr.min(b_sr);
167    let sc = a_sc.min(b_sc);
168    let er = a_er.max(b_er);
169    let ec = a_ec.max(b_ec);
170
171    Ok(ReferenceType::Range {
172        sheet: sheet_a,
173        start_row: Some(sr),
174        start_col: Some(sc),
175        end_row: Some(er),
176        end_col: Some(ec),
177    })
178}
179
180impl fmt::Display for Coord {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        if self.col_abs() {
183            write!(f, "$")?;
184        }
185        write!(f, "{}", Self::col_to_letters(self.col()))?;
186        if self.row_abs() {
187            write!(f, "$")?;
188        }
189        // rows are 1‑based in A1 notation
190        write!(f, "{}", self.row() + 1)
191    }
192}
193
194//------------------------------------------------------------------------------
195// CellRef
196//------------------------------------------------------------------------------
197
198/// Sheet identifier inside a workbook.
199///
200/// Sheet ids are assigned by the engine/registry and have no sentinel values.
201pub type SheetId = u16; // 65,535 sheets should be enough for anyone.
202
203#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
204pub struct CellRef {
205    pub sheet_id: SheetId,
206    pub coord: Coord,
207}
208
209impl CellRef {
210    #[inline]
211    pub const fn new(sheet_id: SheetId, coord: Coord) -> Self {
212        Self { sheet_id, coord }
213    }
214
215    #[inline]
216    pub fn new_absolute(sheet_id: SheetId, row: u32, col: u32) -> Self {
217        Self {
218            sheet_id,
219            coord: Coord::new(row, col, true, true),
220        }
221    }
222
223    /// Rebase using underlying `Coord` logic.
224    #[inline]
225    pub fn rebase(self, origin: Coord, target: Coord) -> Self {
226        Self {
227            sheet_id: self.sheet_id,
228            coord: self.coord.rebase(origin, target),
229        }
230    }
231
232    #[inline]
233    pub fn sheet_name<'a>(&self, sheet_reg: &'a SheetRegistry) -> &'a str {
234        sheet_reg.name(self.sheet_id)
235    }
236
237    #[inline]
238    pub fn to_shared(self) -> SharedCellRef<'static> {
239        SharedCellRef::new(
240            SharedSheetLocator::Id(self.sheet_id),
241            self.coord.into_inner(),
242        )
243    }
244
245    pub fn try_from_shared(cell: SharedCellRef<'_>) -> Result<Self, ExcelError> {
246        let owned = cell.into_owned();
247        let sheet_id = match owned.sheet {
248            SharedSheetLocator::Id(id) => id,
249            _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
250        };
251        Ok(Self::new(sheet_id, Coord(owned.coord)))
252    }
253}
254
255impl fmt::Display for CellRef {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        // Always include the sheet id; there is no longer a "current sheet" sentinel.
258        write!(f, "Sheet{}!", self.sheet_id)?;
259        write!(f, "{}", self.coord)
260    }
261}
262
263//------------------------------------------------------------------------------
264// RangeRef (half‑open range helper)
265//------------------------------------------------------------------------------
266
267#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
268pub struct RangeRef {
269    pub start: CellRef,
270    pub end: CellRef, // inclusive like Excel: A1:B5 covers both corners
271}
272
273impl RangeRef {
274    #[inline]
275    pub const fn new(start: CellRef, end: CellRef) -> Self {
276        Self { start, end }
277    }
278
279    pub fn try_to_shared(self) -> Result<SharedRangeRef<'static>, ExcelError> {
280        if self.start.sheet_id != self.end.sheet_id {
281            return Err(ExcelError::new(ExcelErrorKind::Ref));
282        }
283        let sheet = SharedSheetLocator::Id(self.start.sheet_id);
284        let sr =
285            formualizer_common::AxisBound::new(self.start.coord.row(), self.start.coord.row_abs());
286        let sc =
287            formualizer_common::AxisBound::new(self.start.coord.col(), self.start.coord.col_abs());
288        let er = formualizer_common::AxisBound::new(self.end.coord.row(), self.end.coord.row_abs());
289        let ec = formualizer_common::AxisBound::new(self.end.coord.col(), self.end.coord.col_abs());
290        SharedRangeRef::from_parts(sheet, Some(sr), Some(sc), Some(er), Some(ec))
291            .map_err(|_| ExcelError::new(ExcelErrorKind::Ref))
292    }
293
294    pub fn try_from_shared(range: SharedRangeRef<'_>) -> Result<Self, ExcelError> {
295        let owned = range.into_owned();
296        let sheet_id = match owned.sheet {
297            SharedSheetLocator::Id(id) => id,
298            _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
299        };
300        let (sr, sc, er, ec) = match (
301            owned.start_row,
302            owned.start_col,
303            owned.end_row,
304            owned.end_col,
305        ) {
306            (Some(sr), Some(sc), Some(er), Some(ec)) => (sr, sc, er, ec),
307            _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
308        };
309        let start = CellRef::new(sheet_id, Coord::new(sr.index, sc.index, sr.abs, sc.abs));
310        let end = CellRef::new(sheet_id, Coord::new(er.index, ec.index, er.abs, ec.abs));
311        Ok(Self::new(start, end))
312    }
313}
314
315impl fmt::Display for RangeRef {
316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317        if self.start.sheet_id == self.end.sheet_id {
318            // Single sheet: prefix once
319            write!(f, "{}:{}", self.start, self.end.coord)
320        } else {
321            // Different sheets: print fully.
322            write!(f, "{}:{}", self.start, self.end)
323        }
324    }
325}
326
327//------------------------------------------------------------------------------
328// Tests
329//------------------------------------------------------------------------------
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn test_display_coord() {
337        let c = Coord::new(0, 0, false, false);
338        assert_eq!(c.to_string(), "A1");
339        let c = Coord::new(7, 27, true, true); // row 8, col 28 == AB
340        assert_eq!(c.to_string(), "$AB$8");
341    }
342
343    #[test]
344    fn test_rebase() {
345        let origin = Coord::new(0, 0, false, false);
346        let target = Coord::new(1, 1, false, false);
347        let formula_coord = Coord::new(2, 0, false, true); // A3 with absolute col
348        let rebased = formula_coord.rebase(origin, target);
349        // Should move down 1 row, col stays because absolute
350        assert_eq!(rebased, Coord::new(3, 0, false, true));
351    }
352
353    #[test]
354    fn test_range_display() {
355        let a1 = CellRef::new(0, Coord::new(0, 0, false, false));
356        let b2 = CellRef::new(0, Coord::new(1, 1, false, false));
357        let r = RangeRef::new(a1, b2);
358        assert_eq!(r.to_string(), "Sheet0!A1:B2");
359    }
360}