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::{ExcelError, ExcelErrorKind};
27use formualizer_parse::parser::ReferenceType;
28
29//------------------------------------------------------------------------------
30// Coord
31//------------------------------------------------------------------------------
32
33/// One 2‑D grid coordinate (row, column) **plus** absolute/relative flags.
34///
35/// * `row` and `col` are *zero‑based* indices.
36/// * `flags` is a 2‑bit field: `bit0 = row_abs`, `bit1 = col_abs`.
37#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
38pub struct Coord {
39    pub row: u32,
40    pub col: u32,
41    flags: u8,
42}
43
44impl Coord {
45    /// Creates a new coordinate.
46    #[inline]
47    pub const fn new(row: u32, col: u32, row_abs: bool, col_abs: bool) -> Self {
48        let flags = (row_abs as u8) | ((col_abs as u8) << 1);
49        Self { row, col, flags }
50    }
51
52    /// Absolute/relative accessors.
53    #[inline]
54    pub const fn row_abs(self) -> bool {
55        self.flags & 0b01 != 0
56    }
57    #[inline]
58    pub const fn col_abs(self) -> bool {
59        self.flags & 0b10 != 0
60    }
61
62    /// Returns a copy with modified row anchor.
63    #[inline]
64    pub const fn with_row_abs(mut self, abs: bool) -> Self {
65        if abs {
66            self.flags |= 0b01
67        } else {
68            self.flags &= !0b01;
69        }
70        self
71    }
72    /// Returns a copy with modified col anchor.
73    #[inline]
74    pub const fn with_col_abs(mut self, abs: bool) -> Self {
75        if abs {
76            self.flags |= 0b10
77        } else {
78            self.flags &= !0b10;
79        }
80        self
81    }
82
83    /// Offset by signed deltas *ignoring* anchor flags (internal helper).
84    #[inline]
85    pub const fn offset(self, drow: i32, dcol: i32) -> Self {
86        Self {
87            row: ((self.row as i32) + drow) as u32,
88            col: ((self.col as i32) + dcol) as u32,
89            flags: self.flags,
90        }
91    }
92
93    /// Re‐base this coordinate as if the *formula containing it* was copied
94    /// from `origin` to `target`.
95    #[inline]
96    pub fn rebase(self, origin: Coord, target: Coord) -> Self {
97        let drow = target.row as i32 - origin.row as i32;
98        let dcol = target.col as i32 - origin.col as i32;
99        let new_row = if self.row_abs() {
100            self.row
101        } else {
102            ((self.row as i32) + drow) as u32
103        };
104        let new_col = if self.col_abs() {
105            self.col
106        } else {
107            ((self.col as i32) + dcol) as u32
108        };
109        Self {
110            row: new_row,
111            col: new_col,
112            flags: self.flags,
113        }
114    }
115
116    // ---- helpers for column letter ↔ index -------------------------------------------------- //
117
118    /// Convert `col` into Excel‑style letters (0‑based ⇒ A, B, …, AA…).
119    pub fn col_to_letters(mut col: u32) -> String {
120        // worst‑case 16,384 cols ⇒ "XFD" (3 chars); keep 8‑char buffer for safety.
121        let mut buf = String::new();
122        loop {
123            let rem = (col % 26) as u8;
124            buf.push(char::from(b'A' + rem));
125            col /= 26;
126            if col == 0 {
127                break;
128            }
129            col -= 1; // shift because Excel letters are 1‑based internally
130        }
131        buf.chars().rev().collect()
132    }
133
134    /// Convert Excel letters (e.g., "AA") back to 0‑based column index.
135    pub fn letters_to_col(s: &str) -> Option<u32> {
136        let mut col: u32 = 0;
137        for (i, ch) in s.bytes().enumerate() {
138            if !ch.is_ascii_uppercase() {
139                return None;
140            }
141            let val = (ch - b'A') as u32;
142            col = col * 26 + val;
143            if i != s.len() - 1 {
144                col += 1; // inverse of the post‑decrement above
145            }
146        }
147        Some(col)
148    }
149}
150
151type SheetBounds = (Option<String>, (u32, u32, u32, u32));
152
153/// Combine two references with the range operator ':'
154/// Supports combining Cell:Cell, Cell:Range (and Range:Cell), and Range:Range on the same sheet.
155/// Returns #REF! for cross-sheet combinations or incompatible shapes.
156pub fn combine_references(
157    a: &ReferenceType,
158    b: &ReferenceType,
159) -> Result<ReferenceType, ExcelError> {
160    // Extract sheet and bounds as (sheet, (sr, sc, er, ec))
161    fn to_bounds(r: &ReferenceType) -> Option<SheetBounds> {
162        match r {
163            ReferenceType::Cell { sheet, row, col } => {
164                Some((sheet.clone(), (*row, *col, *row, *col)))
165            }
166            ReferenceType::Range {
167                sheet,
168                start_row,
169                start_col,
170                end_row,
171                end_col,
172            } => {
173                let (sr, sc, er, ec) = match (start_row, start_col, end_row, end_col) {
174                    (Some(sr), Some(sc), Some(er), Some(ec)) => (*sr, *sc, *er, *ec),
175                    _ => return None,
176                };
177                Some((sheet.clone(), (sr, sc, er, ec)))
178            }
179            _ => None,
180        }
181    }
182
183    let (sheet_a, (a_sr, a_sc, a_er, a_ec)) = to_bounds(a).ok_or_else(|| {
184        ExcelError::new(ExcelErrorKind::Ref).with_message("Unsupported reference for ':'")
185    })?;
186    let (sheet_b, (b_sr, b_sc, b_er, b_ec)) = to_bounds(b).ok_or_else(|| {
187        ExcelError::new(ExcelErrorKind::Ref).with_message("Unsupported reference for ':'")
188    })?;
189
190    // Sheets must match (both None or equal Some)
191    if sheet_a != sheet_b {
192        return Err(ExcelError::new(ExcelErrorKind::Ref)
193            .with_message("Cannot combine references across sheets"));
194    }
195
196    let sr = a_sr.min(b_sr);
197    let sc = a_sc.min(b_sc);
198    let er = a_er.max(b_er);
199    let ec = a_ec.max(b_ec);
200
201    Ok(ReferenceType::Range {
202        sheet: sheet_a,
203        start_row: Some(sr),
204        start_col: Some(sc),
205        end_row: Some(er),
206        end_col: Some(ec),
207    })
208}
209
210impl fmt::Display for Coord {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        if self.col_abs() {
213            write!(f, "$")?;
214        }
215        write!(f, "{}", Self::col_to_letters(self.col))?;
216        if self.row_abs() {
217            write!(f, "$")?;
218        }
219        // rows are 1‑based in A1 notation
220        write!(f, "{}", self.row + 1)
221    }
222}
223
224//------------------------------------------------------------------------------
225// CellRef
226//------------------------------------------------------------------------------
227
228/// Sheet identifier inside a workbook.
229///
230/// A `SheetId` of `0` is a special value representing the sheet
231/// that contains the reference itself.
232pub type SheetId = u16; // 65,535 sheets should be enough for anyone.
233
234#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
235pub struct CellRef {
236    pub sheet_id: SheetId, // 0 == current sheet fast‑path
237    pub coord: Coord,
238}
239
240impl CellRef {
241    #[inline]
242    pub const fn new(sheet_id: SheetId, coord: Coord) -> Self {
243        Self { sheet_id, coord }
244    }
245
246    #[inline]
247    pub fn new_absolute(sheet_id: SheetId, row: u32, col: u32) -> Self {
248        Self {
249            sheet_id,
250            coord: Coord::new(row, col, true, true),
251        }
252    }
253
254    /// Rebase using underlying `Coord` logic.
255    #[inline]
256    pub fn rebase(self, origin: Coord, target: Coord) -> Self {
257        Self {
258            sheet_id: self.sheet_id,
259            coord: self.coord.rebase(origin, target),
260        }
261    }
262
263    #[inline]
264    pub fn sheet_name<'a>(&self, sheet_reg: &'a SheetRegistry) -> &'a str {
265        sheet_reg.name(self.sheet_id)
266    }
267}
268
269impl fmt::Display for CellRef {
270    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271        if self.sheet_id != 0 {
272            write!(f, "Sheet{}!", self.sheet_id)?; // caller can map id→name if needed
273        }
274        write!(f, "{}", self.coord)
275    }
276}
277
278//------------------------------------------------------------------------------
279// RangeRef (half‑open range helper)
280//------------------------------------------------------------------------------
281
282#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
283pub struct RangeRef {
284    pub start: CellRef,
285    pub end: CellRef, // inclusive like Excel: A1:B5 covers both corners
286}
287
288impl RangeRef {
289    #[inline]
290    pub const fn new(start: CellRef, end: CellRef) -> Self {
291        Self { start, end }
292    }
293}
294
295impl fmt::Display for RangeRef {
296    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297        if self.start.sheet_id == self.end.sheet_id {
298            // Single sheet: prefix once
299            write!(f, "{}:{}", self.start, self.end.coord)
300        } else {
301            // Different sheets: print fully.
302            write!(f, "{}:{}", self.start, self.end)
303        }
304    }
305}
306
307//------------------------------------------------------------------------------
308// Tests
309//------------------------------------------------------------------------------
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn test_display_coord() {
317        let c = Coord::new(0, 0, false, false);
318        assert_eq!(c.to_string(), "A1");
319        let c = Coord::new(7, 27, true, true); // row 8, col 28 == AB
320        assert_eq!(c.to_string(), "$AB$8");
321    }
322
323    #[test]
324    fn test_rebase() {
325        let origin = Coord::new(0, 0, false, false);
326        let target = Coord::new(1, 1, false, false);
327        let formula_coord = Coord::new(2, 0, false, true); // A3 with absolute col
328        let rebased = formula_coord.rebase(origin, target);
329        // Should move down 1 row, col stays because absolute
330        assert_eq!(rebased, Coord::new(3, 0, false, true));
331    }
332
333    #[test]
334    fn test_range_display() {
335        let a1 = CellRef::new(0, Coord::new(0, 0, false, false));
336        let b2 = CellRef::new(0, Coord::new(1, 1, false, false));
337        let r = RangeRef::new(a1, b2);
338        assert_eq!(r.to_string(), "A1:B2");
339    }
340}