Skip to main content

formualizer_common/
error.rs

1//! Excel-style error representation that is both ergonomic **now**
2//! *and* flexible enough to grow new, data-rich variants later.
3//!
4//! - **`ExcelErrorKind`** : the canonical set of Excel error codes  
5//! - **`ErrorContext`**   : lightweight, sheet-agnostic location info  
6//! - **`ExcelErrorExtra`**: per-kind “extension slot” (e.g. `Spill`)  
7//! - **`ExcelError`**     : one struct that glues the three together
8//!
9//! When a future error needs its own payload, just add another variant
10//! to `ExcelErrorExtra`; existing code does not break.
11
12use std::{error::Error, fmt};
13
14#[cfg(feature = "serde")]
15use serde::{Deserialize, Serialize};
16
17use crate::LiteralValue;
18
19/// All recognised Excel error codes.
20///
21/// **Note:** names are CamelCase (idiomatic Rust) while `Display`
22/// renders them exactly as Excel shows them (`#DIV/0!`, …).
23#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
24#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
25pub enum ExcelErrorKind {
26    Null,
27    Ref,
28    Name,
29    Value,
30    Div,
31    Na,
32    Num,
33    Error,
34    NImpl,
35    Spill,
36    Calc,
37    Circ,
38    Cancelled,
39}
40
41impl fmt::Display for ExcelErrorKind {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.write_str(match self {
44            Self::Null => "#NULL!",
45            Self::Ref => "#REF!",
46            Self::Name => "#NAME?",
47            Self::Value => "#VALUE!",
48            Self::Div => "#DIV/0!",
49            Self::Na => "#N/A",
50            Self::Num => "#NUM!",
51            Self::Error => "#ERROR!",
52            Self::NImpl => "#N/IMPL!",
53            Self::Spill => "#SPILL!",
54            Self::Calc => "#CALC!",
55            Self::Circ => "#CIRC!",
56            Self::Cancelled => "#CANCELLED!",
57        })
58    }
59}
60
61impl ExcelErrorKind {
62    pub fn try_parse(s: &str) -> Option<Self> {
63        match s.trim().to_ascii_lowercase().as_str() {
64            "#null!" => Some(Self::Null),
65            "#ref!" => Some(Self::Ref),
66            "#name?" => Some(Self::Name),
67            "#value!" => Some(Self::Value),
68            "#div/0!" => Some(Self::Div),
69            "#n/a" => Some(Self::Na),
70            "#num!" => Some(Self::Num),
71            "#error!" => Some(Self::Error),
72            "#n/impl!" => Some(Self::NImpl),
73            "#spill!" => Some(Self::Spill),
74            "#calc!" => Some(Self::Calc),
75            "#circ!" => Some(Self::Circ),
76            "#cancelled!" => Some(Self::Cancelled),
77            _ => None,
78        }
79    }
80
81    pub fn parse(s: &str) -> Self {
82        Self::try_parse(s).unwrap_or(Self::Error)
83    }
84}
85
86/// Generic, lightweight metadata that *any* error may carry.
87///
88/// Keep this minimal—anything only one error kind needs belongs in
89/// `ExcelErrorExtra`.
90#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
91#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
92pub struct ErrorContext {
93    pub row: Option<u32>,
94    pub col: Option<u32>,
95    // Origin location where the error first occurred (if different from row/col)
96    pub origin_row: Option<u32>,
97    pub origin_col: Option<u32>,
98    pub origin_sheet: Option<String>,
99}
100
101/// Kind-specific payloads (“extension slot”).
102///
103/// Only variants that need extra data get it—rest stay at `None`.
104#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
105#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
106pub enum ExcelErrorExtra {
107    /// No additional payload (the vast majority of errors).
108    #[default]
109    None,
110
111    /// `#SPILL!` – information about the intended spill size.
112    Spill {
113        expected_rows: u32,
114        expected_cols: u32,
115    },
116    // --- Add future custom payloads below -------------------------------
117    // AnotherKind { … },
118}
119
120/// The single struct your API passes around.
121///
122/// It combines:
123/// * **kind**   – the mandatory Excel error code
124/// * **message**– optional human explanation
125/// * **context**– generic location†
126/// * **extra**  – optional, kind-specific data
127///
128/// † If you *never* need row/col you can build the value with
129///   `ExcelError::from(kind)`, which sets `context = None`.
130#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
131#[derive(Debug, Clone, PartialEq, Eq, Hash)]
132pub struct ExcelError {
133    pub kind: ExcelErrorKind,
134    pub message: Option<String>,
135    pub context: Option<ErrorContext>,
136    pub extra: ExcelErrorExtra,
137}
138
139/* ───────────────────── Constructors & helpers ─────────────────────── */
140
141impl From<ExcelErrorKind> for ExcelError {
142    fn from(kind: ExcelErrorKind) -> Self {
143        Self {
144            kind,
145            message: None,
146            context: None,
147            extra: ExcelErrorExtra::None,
148        }
149    }
150}
151
152impl ExcelError {
153    /// Basic constructor (no message, no location, no extra).
154    pub fn new(kind: ExcelErrorKind) -> Self {
155        kind.into()
156    }
157
158    /// Attach a human-readable explanation.
159    pub fn with_message<S: Into<String>>(mut self, msg: S) -> Self {
160        self.message = Some(msg.into());
161        self
162    }
163
164    /// Attach generic row/column coordinates.
165    pub fn with_location(mut self, row: u32, col: u32) -> Self {
166        self.context = Some(ErrorContext {
167            row: Some(row),
168            col: Some(col),
169            origin_row: None,
170            origin_col: None,
171            origin_sheet: None,
172        });
173        self
174    }
175
176    /// Attach origin location where the error first occurred.
177    pub fn with_origin(mut self, sheet: Option<String>, row: u32, col: u32) -> Self {
178        if let Some(ref mut ctx) = self.context {
179            ctx.origin_sheet = sheet;
180            ctx.origin_row = Some(row);
181            ctx.origin_col = Some(col);
182        } else {
183            self.context = Some(ErrorContext {
184                row: None,
185                col: None,
186                origin_row: Some(row),
187                origin_col: Some(col),
188                origin_sheet: sheet,
189            });
190        }
191        self
192    }
193
194    /// Attach kind-specific extra data.
195    pub fn with_extra(mut self, extra: ExcelErrorExtra) -> Self {
196        self.extra = extra;
197        self
198    }
199
200    pub fn from_error_string(s: &str) -> Self {
201        match ExcelErrorKind::try_parse(s) {
202            Some(kind) => Self::new(kind),
203            None => {
204                Self::new(ExcelErrorKind::Error).with_message(format!("Unknown error code: {s}"))
205            }
206        }
207    }
208
209    pub fn new_value() -> Self {
210        Self::new(ExcelErrorKind::Value)
211    }
212
213    pub fn new_name() -> Self {
214        Self::new(ExcelErrorKind::Name)
215    }
216
217    pub fn new_div() -> Self {
218        Self::new(ExcelErrorKind::Div)
219    }
220
221    pub fn new_ref() -> Self {
222        Self::new(ExcelErrorKind::Ref)
223    }
224
225    pub fn new_circ() -> Self {
226        Self::new(ExcelErrorKind::Circ)
227    }
228
229    pub fn new_num() -> Self {
230        Self::new(ExcelErrorKind::Num)
231    }
232
233    pub fn new_na() -> Self {
234        Self::new(ExcelErrorKind::Na)
235    }
236}
237
238/* ───────────────────────── Display / Error ────────────────────────── */
239
240impl fmt::Display for ExcelError {
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        // Start with the canonical code:
243        write!(f, "{}", self.kind)?;
244
245        // Optional human message.
246        if let Some(ref msg) = self.message {
247            write!(f, ": {msg}")?;
248        }
249
250        // Optional row/col context.
251        if let Some(ref ctx) = self.context {
252            if let (Some(r), Some(c)) = (ctx.row, ctx.col) {
253                write!(f, " (row {r}, col {c})")?;
254            }
255
256            // Show origin if different from the evaluation location
257            if let (Some(or), Some(oc)) = (ctx.origin_row, ctx.origin_col)
258                && (ctx.row != Some(or) || ctx.col != Some(oc))
259            {
260                if let Some(ref sheet) = ctx.origin_sheet {
261                    write!(f, " [origin: {sheet}!R{or}C{oc}]")?;
262                } else {
263                    write!(f, " [origin: R{or}C{oc}]")?;
264                }
265            }
266        }
267
268        // Optional kind-specific payload - keep it terse for logs.
269        match &self.extra {
270            ExcelErrorExtra::None => {}
271            ExcelErrorExtra::Spill {
272                expected_rows,
273                expected_cols,
274            } => {
275                write!(f, " [spill {expected_rows}×{expected_cols}]")?;
276            }
277        }
278
279        Ok(())
280    }
281}
282
283impl Error for ExcelError {}
284impl From<ExcelError> for String {
285    fn from(error: ExcelError) -> Self {
286        format!("{error}")
287    }
288}
289impl From<ExcelError> for LiteralValue {
290    fn from(error: ExcelError) -> Self {
291        LiteralValue::Error(error)
292    }
293}
294
295impl PartialEq<str> for ExcelErrorKind {
296    fn eq(&self, other: &str) -> bool {
297        format!("{self}") == other
298    }
299}
300
301impl PartialEq<&str> for ExcelError {
302    fn eq(&self, other: &&str) -> bool {
303        self.kind.to_string() == *other
304    }
305}
306
307impl PartialEq<str> for ExcelError {
308    fn eq(&self, other: &str) -> bool {
309        self.kind.to_string() == other
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn parse_known_error_kind() {
319        assert_eq!(ExcelErrorKind::parse("#DIV/0!"), ExcelErrorKind::Div);
320        assert_eq!(ExcelErrorKind::parse("#n/a"), ExcelErrorKind::Na);
321    }
322
323    #[test]
324    fn parse_unknown_error_kind_falls_back() {
325        assert_eq!(ExcelErrorKind::parse("#BOGUS!"), ExcelErrorKind::Error);
326        let err = ExcelError::from_error_string("#BOGUS!");
327        assert_eq!(err.kind, ExcelErrorKind::Error);
328        assert!(err.message.unwrap_or_default().contains("#BOGUS!"));
329    }
330}