Skip to main content

visi_core/core/engine/
cell.rs

1use serde::{Deserialize, Serialize};
2
3/// A random 53-bit identifier for a sheet or column.
4///
5/// Capped to `2^53 - 1` so it survives a round trip through a JSON number,
6/// which is what a JavaScript host would deserialize it as. Falls back to the
7/// wall clock if the system random source is unavailable.
8pub fn generate_unique_id() -> u64 {
9    let mut buf = [0u8; 8];
10    let val = if getrandom::getrandom(&mut buf).is_err() {
11        let now = web_time::SystemTime::now()
12            .duration_since(web_time::SystemTime::UNIX_EPOCH)
13            .map(|d| d.as_nanos())
14            .unwrap_or(0);
15        now as u64
16    } else {
17        u64::from_le_bytes(buf)
18    };
19    val & 0x001F_FFFF_FFFF_FFFF
20}
21
22/// [AI-Agent] The intrinsic data type of a cell, mirroring calamine worksheet value variants.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
24pub enum CellType {
25    /// Empty cell (`calamine::Data::Empty`).
26    #[default]
27    Empty,
28    /// Signed integer (`calamine::Data::Int`).
29    Int,
30    /// Floating point number (`calamine::Data::Float`).
31    Float,
32    /// String (`calamine::Data::String`).
33    String,
34    /// Boolean (`calamine::Data::Bool`).
35    Bool,
36    /// Date/time serial identified by calamine from workbook formatting.
37    DateTime,
38    /// ISO 8601 date/time (`calamine::Data::DateTimeIso`, OpenXML `t="d"`).
39    DateTimeIso,
40    /// ISO 8601 duration (`calamine::Data::DurationIso`).
41    DurationIso,
42    /// Error cell (`calamine::Data::Error`, OpenXML `t="e"`).
43    Error,
44}
45
46impl CellType {
47    /// [AI-Agent] Whether this cell type is explicitly a string/text cell.
48    pub fn is_string(&self) -> bool {
49        matches!(self, CellType::String)
50    }
51
52    /// [AI-Agent] Stable lowercase spelling used by CLI and JSON output.
53    pub fn as_str(&self) -> &'static str {
54        match self {
55            CellType::Empty => "empty",
56            CellType::Int => "int",
57            CellType::Float => "float",
58            CellType::String => "string",
59            CellType::Bool => "bool",
60            CellType::DateTime => "date-time",
61            CellType::DateTimeIso => "date-time-iso",
62            CellType::DurationIso => "duration-iso",
63            CellType::Error => "error",
64        }
65    }
66}
67
68/// For either a column or row
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
70pub enum RefType {
71    /// Written without a `$`, so it shifts when the formula is filled or
72    /// copied.
73    Relative,
74    /// Written with a `$`, so it stays put.
75    Absolute,
76}
77
78impl std::fmt::Display for RefType {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        match self {
81            RefType::Relative => write!(f, ""),
82            RefType::Absolute => write!(f, "$"),
83        }
84    }
85}
86
87/// A cell's position, plus whether it was written as absolute.
88///
89/// Coordinates are 0-based, as everywhere inside the engine; `A1` is
90/// `CellRef::new(0, 0)`.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
92pub struct CellRef {
93    /// Row index, 0-based.
94    pub row: usize,
95    /// Column index, 0-based.
96    pub col: usize,
97    /// Whether the row was written with a `$`.
98    pub row_ref_type: RefType,
99    /// Whether the column was written with a `$`.
100    pub col_ref_type: RefType,
101}
102
103impl std::fmt::Display for CellRef {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        write!(
106            f,
107            "CELL({}{}, {}{})",
108            self.row_ref_type, self.row, self.col_ref_type, self.col
109        )
110    }
111}
112
113impl CellRef {
114    /// A relative reference to `(row, col)`, 0-based.
115    pub fn new(row: usize, col: usize) -> CellRef {
116        Self {
117            row,
118            col,
119            row_ref_type: RefType::Relative,
120            col_ref_type: RefType::Relative,
121        }
122    }
123}
124
125/// Something a formula reads, and therefore an edge in the recalculation
126/// graph.
127///
128/// The local/remote split is load-bearing. `Sheet::commit` propagates through
129/// the `Local` variants only -- a sheet cannot reach into its neighbors, so a
130/// remote edge it finds is recorded but not followed. Chasing those is
131/// `WorkbookManager::evaluate`'s job, which marks every sheet dirty and runs a
132/// fixed number of passes over the workbook; a cross-sheet chain deeper than
133/// that number of hops will not have converged when it stops.
134///
135/// Remote variants key on the sheet *name* rather than its id, since that is
136/// what a formula's text carries and what `Context` is indexed by.
137#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
138pub enum Dependency {
139    /// A cell on the same sheet.
140    Local(CellRef),
141    /// A whole column on the same sheet, by 0-based position.
142    LocalColumn(usize),
143    /// A cell on another sheet.
144    Remote {
145        /// Name of the sheet the cell is on.
146        sheet: String,
147        /// The cell, on that sheet.
148        cell: CellRef,
149    },
150    /// A whole column on another sheet.
151    RemoteColumn {
152        /// Name of the sheet the column is on.
153        sheet: String,
154        /// Column index, 0-based.
155        col: usize,
156    },
157}
158
159/// A caret position: a cell plus an offset within its source text, for the
160/// text-editing operations `Sheet::insert` and `Sheet::delete`.
161#[derive(Debug, Clone, Default)]
162pub struct TextCellRef {
163    /// Row index, 0-based.
164    pub row: usize,
165    /// Column index, 0-based.
166    pub col: usize,
167    /// Offset into the cell's source text, in characters rather than bytes.
168    pub char_offset: usize,
169}
170
171/// A formula that could not be evaluated at all.
172///
173/// Distinct from an Excel error value: `=1/0` evaluates successfully to
174/// `ResultData::Error("#DIV/0!")`, whereas this is for text that never became
175/// a computable formula.
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub enum EvalError {
178    /// The formula could not be parsed, or named something unrecognized. The
179    /// string is the message, which for some failures is an Excel error code.
180    UnknownFunction(String),
181}
182
183impl std::error::Error for EvalError {}
184
185impl std::fmt::Display for EvalError {
186    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        match self {
188            EvalError::UnknownFunction(func) => write!(f, "{}", func),
189        }
190    }
191}
192
193/// What the engine's evaluation entry points return on failure.
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub enum EngineError {
196    /// A formula could not be evaluated.
197    EvalError(EvalError),
198}
199
200impl std::error::Error for EngineError {
201    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
202        match self {
203            EngineError::EvalError(err) => Some(err),
204        }
205    }
206}
207
208impl std::fmt::Display for EngineError {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        match self {
211            EngineError::EvalError(err) => write!(f, "{}", err),
212        }
213    }
214}
215
216impl From<EvalError> for EngineError {
217    fn from(err: EvalError) -> Self {
218        EngineError::EvalError(err)
219    }
220}