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