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#[non_exhaustive]
24#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
25#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
26pub enum ExcelErrorKind {
27    Null,
28    Ref,
29    Name,
30    Value,
31    Div,
32    Na,
33    Num,
34    Error,
35    NImpl,
36    Spill,
37    Calc,
38    Circ,
39    Cancelled,
40}
41
42impl fmt::Display for ExcelErrorKind {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.write_str(match self {
45            Self::Null => "#NULL!",
46            Self::Ref => "#REF!",
47            Self::Name => "#NAME?",
48            Self::Value => "#VALUE!",
49            Self::Div => "#DIV/0!",
50            Self::Na => "#N/A",
51            Self::Num => "#NUM!",
52            Self::Error => "#ERROR!",
53            Self::NImpl => "#N/IMPL!",
54            Self::Spill => "#SPILL!",
55            Self::Calc => "#CALC!",
56            Self::Circ => "#CIRC!",
57            Self::Cancelled => "#CANCELLED!",
58        })
59    }
60}
61
62impl ExcelErrorKind {
63    pub fn try_parse(s: &str) -> Option<Self> {
64        match s.trim().to_ascii_lowercase().as_str() {
65            "#null!" => Some(Self::Null),
66            "#ref!" => Some(Self::Ref),
67            "#name?" => Some(Self::Name),
68            "#value!" => Some(Self::Value),
69            "#div/0!" => Some(Self::Div),
70            "#n/a" => Some(Self::Na),
71            "#num!" => Some(Self::Num),
72            "#error!" => Some(Self::Error),
73            "#n/impl!" => Some(Self::NImpl),
74            "#spill!" => Some(Self::Spill),
75            "#calc!" => Some(Self::Calc),
76            "#circ!" => Some(Self::Circ),
77            "#cancelled!" => Some(Self::Cancelled),
78            _ => None,
79        }
80    }
81
82    pub fn parse(s: &str) -> Self {
83        Self::try_parse(s).unwrap_or(Self::Error)
84    }
85}
86
87/// Generic, lightweight metadata that *any* error may carry.
88///
89/// Keep this minimal—anything only one error kind needs belongs in
90/// `ExcelErrorExtra`.
91#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
92#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
93pub struct ErrorContext {
94    pub row: Option<u32>,
95    pub col: Option<u32>,
96    // Origin location where the error first occurred (if different from row/col)
97    pub origin_row: Option<u32>,
98    pub origin_col: Option<u32>,
99    pub origin_sheet: Option<String>,
100}
101
102/// Stable reason for a resource exhaustion diagnostic.
103#[non_exhaustive]
104#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
105#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
106pub enum ResourceExhaustionReason {
107    Admission,
108    RetainedMemory,
109    ScratchMemory,
110    WorkUnits,
111    Deadline,
112    GraphVertices,
113    GraphEdges,
114    MaterializationCells,
115    OperationEvidenceRoots,
116    OperationEvidenceTextBytes,
117    /// Combined affected-region and uncertainty items, not grid cells/bytes.
118    DependencyClosureOutputItems,
119    ArithmeticOverflow,
120}
121
122impl ResourceExhaustionReason {
123    pub const fn as_str(self) -> &'static str {
124        match self {
125            Self::Admission => "admission",
126            Self::RetainedMemory => "retained_memory",
127            Self::ScratchMemory => "scratch_memory",
128            Self::WorkUnits => "work_units",
129            Self::Deadline => "deadline",
130            Self::GraphVertices => "graph_vertices",
131            Self::GraphEdges => "graph_edges",
132            Self::MaterializationCells => "materialization_cells",
133            Self::OperationEvidenceRoots => "operation_evidence_roots",
134            Self::OperationEvidenceTextBytes => "operation_evidence_text_bytes",
135            Self::DependencyClosureOutputItems => "dependency_closure_output_items",
136            Self::ArithmeticOverflow => "arithmetic_overflow",
137        }
138    }
139}
140
141/// Binding-safe typed resource exhaustion payload.
142#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
143#[derive(Debug, Clone, PartialEq, Eq, Hash)]
144pub struct ResourceExhaustionDetail {
145    pub reason: ResourceExhaustionReason,
146    pub limit: u64,
147    pub observed: u64,
148    pub request_id: Option<u64>,
149}
150
151/// Stable category for a target-preparation plan rejected at its final boundary.
152#[non_exhaustive]
153#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
154#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
155pub enum PreparationStaleReason {
156    Graph,
157    Authority,
158    Staged,
159    Symbols,
160    Semantic,
161    Provider,
162}
163
164impl PreparationStaleReason {
165    pub const fn as_str(self) -> &'static str {
166        match self {
167            Self::Graph => "graph",
168            Self::Authority => "authority",
169            Self::Staged => "staged",
170            Self::Symbols => "symbols",
171            Self::Semantic => "semantic",
172            Self::Provider => "provider",
173        }
174    }
175}
176
177/// Stable category for a reusable recalculation plan rejected before execution.
178#[non_exhaustive]
179#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
180#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
181pub enum PlanStaleReason {
182    Engine,
183    Provider,
184    Semantic,
185    Budget,
186    Staged,
187    Symbols,
188    Authority,
189    SpanGeneration,
190    Graph,
191}
192
193impl PlanStaleReason {
194    pub const fn as_str(self) -> &'static str {
195        match self {
196            Self::Engine => "engine",
197            Self::Provider => "provider",
198            Self::Semantic => "semantic",
199            Self::Budget => "budget",
200            Self::Staged => "staged",
201            Self::Symbols => "symbols",
202            Self::Authority => "authority",
203            Self::SpanGeneration => "span_generation",
204            Self::Graph => "graph",
205        }
206    }
207}
208
209/// Kind-specific payloads (“extension slot”).
210///
211/// Only variants that need extra data get it—rest stay at `None`.
212#[non_exhaustive]
213#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
214#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
215pub enum ExcelErrorExtra {
216    /// No additional payload (the vast majority of errors).
217    #[default]
218    None,
219
220    /// An evaluation request was refused because an explicit recalculation job is active.
221    RecalcJobActive,
222
223    /// A Rust function unwound. Non-string panic payloads have no message.
224    FunctionPanicked {
225        function: String,
226        message: Option<String>,
227    },
228
229    /// An earlier engine-internal unwind makes further work unsafe.
230    EnginePoisoned,
231
232    /// `#SPILL!` – information about the intended spill size.
233    Spill {
234        expected_rows: u32,
235        expected_cols: u32,
236    },
237
238    /// Typed resource exhaustion. The canonical Excel error kind remains
239    /// unchanged so existing bindings and formula error handling stay compatible.
240    Resource {
241        detail: Box<ResourceExhaustionDetail>,
242    },
243
244    PreparationStale {
245        reason: PreparationStaleReason,
246    },
247
248    PlanStale {
249        reason: PlanStaleReason,
250    },
251    // --- Add future custom payloads below -------------------------------
252    // AnotherKind { … },
253}
254
255/// The single struct your API passes around.
256///
257/// It combines:
258/// * **kind**   – the mandatory Excel error code
259/// * **message**– optional human explanation
260/// * **context**– generic location†
261/// * **extra**  – optional, kind-specific data
262///
263/// † If you *never* need row/col you can build the value with
264///   `ExcelError::from(kind)`, which sets `context = None`.
265#[derive(Clone, PartialEq, Eq)]
266pub struct ExcelError {
267    pub kind: ExcelErrorKind,
268    // Empty metadata is always represented by None, keeping derived equality logical.
269    payload: Option<Box<ExcelErrorPayload>>,
270}
271
272#[derive(Clone, PartialEq, Eq, Default)]
273struct ExcelErrorPayload {
274    message: Option<String>,
275    context: Option<ErrorContext>,
276    extra: ExcelErrorExtra,
277}
278
279// Keep the logical fields (rather than the storage representation) observable.
280impl fmt::Debug for ExcelError {
281    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282        f.debug_struct("ExcelError")
283            .field("kind", &self.kind)
284            .field("message", &self.message())
285            .field("context", &self.context())
286            .field("extra", self.extra())
287            .finish()
288    }
289}
290
291impl std::hash::Hash for ExcelError {
292    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
293        self.kind.hash(state);
294        self.message().hash(state);
295        self.context().hash(state);
296        self.extra().hash(state);
297    }
298}
299
300#[cfg(feature = "serde")]
301impl Serialize for ExcelError {
302    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
303        use serde::ser::SerializeStruct;
304        let mut fields = serializer.serialize_struct("ExcelError", 4)?;
305        fields.serialize_field("kind", &self.kind)?;
306        fields.serialize_field("message", &self.message())?;
307        fields.serialize_field("context", &self.context())?;
308        fields.serialize_field("extra", self.extra())?;
309        fields.end()
310    }
311}
312
313#[cfg(feature = "serde")]
314impl<'de> Deserialize<'de> for ExcelError {
315    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
316        #[derive(Deserialize)]
317        #[serde(rename = "ExcelError")]
318        struct Fields {
319            kind: ExcelErrorKind,
320            message: Option<String>,
321            context: Option<ErrorContext>,
322            extra: ExcelErrorExtra,
323        }
324        let fields = Fields::deserialize(deserializer)?;
325        let mut error = ExcelError::new(fields.kind);
326        error.set_message(fields.message);
327        error.set_context(fields.context);
328        error.set_extra(fields.extra);
329        Ok(error)
330    }
331}
332
333/* ───────────────────── Constructors & helpers ─────────────────────── */
334
335impl From<ExcelErrorKind> for ExcelError {
336    fn from(kind: ExcelErrorKind) -> Self {
337        Self {
338            kind,
339            payload: None,
340        }
341    }
342}
343
344impl ExcelError {
345    /// Convert an unwinding Rust function panic to a typed #VALUE! error.
346    pub fn from_function_panic(function: &str, payload: &(dyn std::any::Any + Send)) -> Self {
347        let message = payload
348            .downcast_ref::<&str>()
349            .map(|s| (*s).to_owned())
350            .or_else(|| payload.downcast_ref::<String>().cloned());
351        Self::new(ExcelErrorKind::Value)
352            .with_message("Function callback panicked")
353            .with_extra(ExcelErrorExtra::FunctionPanicked {
354                function: function.to_owned(),
355                message,
356            })
357    }
358
359    /// Basic constructor (no message, no location, no extra).
360    pub fn new(kind: ExcelErrorKind) -> Self {
361        kind.into()
362    }
363
364    /// Human-readable explanation, if supplied.
365    pub fn message(&self) -> Option<&str> {
366        self.payload
367            .as_ref()
368            .and_then(|payload| payload.message.as_deref())
369    }
370
371    /// Generic location metadata, if supplied.
372    pub fn context(&self) -> Option<&ErrorContext> {
373        self.payload
374            .as_ref()
375            .and_then(|payload| payload.context.as_ref())
376    }
377
378    /// Kind-specific data; payload-free errors return `ExcelErrorExtra::None`.
379    pub fn extra(&self) -> &ExcelErrorExtra {
380        self.payload
381            .as_ref()
382            .map_or(&ExcelErrorExtra::None, |payload| &payload.extra)
383    }
384
385    fn payload_mut(&mut self) -> &mut ExcelErrorPayload {
386        self.payload.get_or_insert_default()
387    }
388
389    fn discard_empty_payload(&mut self) {
390        if self.payload.as_ref().is_some_and(|payload| {
391            payload.message.is_none()
392                && payload.context.is_none()
393                && payload.extra == ExcelErrorExtra::None
394        }) {
395            self.payload = None;
396        }
397    }
398
399    /// Replace or clear the explanation, retaining any other metadata.
400    pub fn set_message(&mut self, message: Option<String>) {
401        if message.is_some() || self.payload.is_some() {
402            self.payload_mut().message = message;
403            self.discard_empty_payload();
404        }
405    }
406
407    /// Replace or clear the location, retaining any other metadata.
408    pub fn set_context(&mut self, context: Option<ErrorContext>) {
409        if context.is_some() || self.payload.is_some() {
410            self.payload_mut().context = context;
411            self.discard_empty_payload();
412        }
413    }
414
415    /// Replace or clear the kind-specific data, retaining any other metadata.
416    pub fn set_extra(&mut self, extra: ExcelErrorExtra) {
417        if extra != ExcelErrorExtra::None || self.payload.is_some() {
418            self.payload_mut().extra = extra;
419            self.discard_empty_payload();
420        }
421    }
422
423    /// Attach a human-readable explanation.
424    pub fn with_message<S: Into<String>>(mut self, msg: S) -> Self {
425        self.set_message(Some(msg.into()));
426        self
427    }
428
429    /// Attach generic row/column coordinates.
430    pub fn with_location(mut self, row: u32, col: u32) -> Self {
431        self.set_context(Some(ErrorContext {
432            row: Some(row),
433            col: Some(col),
434            origin_row: None,
435            origin_col: None,
436            origin_sheet: None,
437        }));
438        self
439    }
440
441    /// Attach origin location where the error first occurred.
442    pub fn with_origin(mut self, sheet: Option<String>, row: u32, col: u32) -> Self {
443        let ctx = self.payload_mut().context.get_or_insert_default();
444        ctx.origin_sheet = sheet;
445        ctx.origin_row = Some(row);
446        ctx.origin_col = Some(col);
447        self
448    }
449
450    /// Attach kind-specific extra data.
451    pub fn with_extra(mut self, extra: ExcelErrorExtra) -> Self {
452        self.set_extra(extra);
453        self
454    }
455
456    pub fn from_error_string(s: &str) -> Self {
457        match ExcelErrorKind::try_parse(s) {
458            Some(kind) => Self::new(kind),
459            None => {
460                Self::new(ExcelErrorKind::Error).with_message(format!("Unknown error code: {s}"))
461            }
462        }
463    }
464
465    pub fn new_value() -> Self {
466        Self::new(ExcelErrorKind::Value)
467    }
468
469    pub fn new_name() -> Self {
470        Self::new(ExcelErrorKind::Name)
471    }
472
473    pub fn new_div() -> Self {
474        Self::new(ExcelErrorKind::Div)
475    }
476
477    pub fn new_ref() -> Self {
478        Self::new(ExcelErrorKind::Ref)
479    }
480
481    pub fn new_circ() -> Self {
482        Self::new(ExcelErrorKind::Circ)
483    }
484
485    pub fn new_num() -> Self {
486        Self::new(ExcelErrorKind::Num)
487    }
488
489    pub fn new_na() -> Self {
490        Self::new(ExcelErrorKind::Na)
491    }
492}
493
494/* ───────────────────────── Display / Error ────────────────────────── */
495
496impl fmt::Display for ExcelError {
497    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
498        // Start with the canonical code:
499        write!(f, "{}", self.kind)?;
500
501        // Optional human message.
502        if let Some(msg) = self.message() {
503            write!(f, ": {msg}")?;
504        }
505
506        // Optional row/col context.
507        if let Some(ctx) = self.context() {
508            if let (Some(r), Some(c)) = (ctx.row, ctx.col) {
509                write!(f, " (row {r}, col {c})")?;
510            }
511
512            // Show origin if different from the evaluation location
513            if let (Some(or), Some(oc)) = (ctx.origin_row, ctx.origin_col) {
514                if ctx.row != Some(or) || ctx.col != Some(oc) {
515                    if let Some(ref sheet) = ctx.origin_sheet {
516                        write!(f, " [origin: {sheet}!R{or}C{oc}]")?;
517                    } else {
518                        write!(f, " [origin: R{or}C{oc}]")?;
519                    }
520                }
521            }
522        }
523
524        // Optional kind-specific payload - keep it terse for logs.
525        match self.extra() {
526            ExcelErrorExtra::None
527            | ExcelErrorExtra::RecalcJobActive
528            | ExcelErrorExtra::FunctionPanicked { .. }
529            | ExcelErrorExtra::EnginePoisoned => {}
530            ExcelErrorExtra::Spill {
531                expected_rows,
532                expected_cols,
533            } => {
534                write!(f, " [spill {expected_rows}×{expected_cols}]")?;
535            }
536            ExcelErrorExtra::Resource { detail } => {
537                write!(
538                    f,
539                    " [resource {} {}/{}]",
540                    detail.reason.as_str(),
541                    detail.observed,
542                    detail.limit
543                )?;
544            }
545            ExcelErrorExtra::PreparationStale { reason } => {
546                write!(f, " [preparation stale {}]", reason.as_str())?;
547            }
548            ExcelErrorExtra::PlanStale { reason } => {
549                write!(f, " [plan stale {}]", reason.as_str())?;
550            }
551        }
552
553        Ok(())
554    }
555}
556
557impl Error for ExcelError {}
558impl From<ExcelError> for String {
559    fn from(error: ExcelError) -> Self {
560        format!("{error}")
561    }
562}
563impl From<ExcelError> for LiteralValue {
564    fn from(error: ExcelError) -> Self {
565        LiteralValue::Error(error)
566    }
567}
568
569impl PartialEq<str> for ExcelErrorKind {
570    fn eq(&self, other: &str) -> bool {
571        format!("{self}") == other
572    }
573}
574
575impl PartialEq<&str> for ExcelError {
576    fn eq(&self, other: &&str) -> bool {
577        self.kind.to_string() == *other
578    }
579}
580
581impl PartialEq<str> for ExcelError {
582    fn eq(&self, other: &str) -> bool {
583        self.kind.to_string() == other
584    }
585}
586
587#[cfg(test)]
588mod tests {
589    use super::*;
590
591    #[test]
592    fn parse_known_error_kind() {
593        assert_eq!(ExcelErrorKind::parse("#DIV/0!"), ExcelErrorKind::Div);
594        assert_eq!(ExcelErrorKind::parse("#n/a"), ExcelErrorKind::Na);
595    }
596
597    #[test]
598    fn parse_unknown_error_kind_falls_back() {
599        assert_eq!(ExcelErrorKind::parse("#BOGUS!"), ExcelErrorKind::Error);
600        let err = ExcelError::from_error_string("#BOGUS!");
601        assert_eq!(err.kind, ExcelErrorKind::Error);
602        assert!(err.message().unwrap_or_default().contains("#BOGUS!"));
603    }
604
605    #[test]
606    fn preparation_stale_reason_has_stable_snake_case_names() {
607        assert_eq!(PreparationStaleReason::Graph.as_str(), "graph");
608        assert_eq!(PreparationStaleReason::Authority.as_str(), "authority");
609        assert_eq!(PreparationStaleReason::Staged.as_str(), "staged");
610        assert_eq!(PreparationStaleReason::Symbols.as_str(), "symbols");
611        assert_eq!(PreparationStaleReason::Semantic.as_str(), "semantic");
612        assert_eq!(PreparationStaleReason::Provider.as_str(), "provider");
613    }
614
615    #[test]
616    fn plan_stale_reason_has_stable_snake_case_names() {
617        assert_eq!(PlanStaleReason::Engine.as_str(), "engine");
618        assert_eq!(PlanStaleReason::Provider.as_str(), "provider");
619        assert_eq!(PlanStaleReason::Semantic.as_str(), "semantic");
620        assert_eq!(PlanStaleReason::Budget.as_str(), "budget");
621        assert_eq!(PlanStaleReason::Staged.as_str(), "staged");
622        assert_eq!(PlanStaleReason::Symbols.as_str(), "symbols");
623        assert_eq!(PlanStaleReason::Authority.as_str(), "authority");
624        assert_eq!(PlanStaleReason::SpanGeneration.as_str(), "span_generation");
625        assert_eq!(PlanStaleReason::Graph.as_str(), "graph");
626    }
627}
628
629#[cfg(test)]
630mod compact_tests {
631    use super::*;
632    use std::hash::{Hash, Hasher};
633
634    #[test]
635    fn compact_layouts() {
636        assert!(size_of::<ExcelError>() <= 16);
637        assert!(size_of::<LiteralValue>() <= 32);
638        assert!(size_of::<Result<LiteralValue, ExcelError>>() <= 40);
639    }
640
641    #[test]
642    fn empty_errors_do_not_have_a_payload() {
643        let mut error = ExcelError::new_value().with_extra(ExcelErrorExtra::None);
644        error.set_message(None);
645        error.set_context(None);
646        assert!(error.payload.is_none());
647        error.set_message(Some(String::new()));
648        assert!(error.payload.is_some());
649        error.set_context(Some(ErrorContext::default()));
650        error.set_extra(ExcelErrorExtra::Spill {
651            expected_rows: 2,
652            expected_cols: 3,
653        });
654        error.set_message(None);
655        error.set_context(None);
656        assert!(error.payload.is_some());
657        error.set_extra(ExcelErrorExtra::None);
658        assert!(error.payload.is_none());
659        assert_eq!(error, ExcelError::new_value());
660    }
661
662    #[test]
663    fn logical_debug_hash_equality_and_display_are_unchanged() {
664        #[derive(Debug, Hash)]
665        struct ExcelError {
666            kind: ExcelErrorKind,
667            message: Option<String>,
668            context: Option<ErrorContext>,
669            extra: ExcelErrorExtra,
670        }
671        fn hash(value: &impl Hash) -> u64 {
672            let mut state = std::collections::hash_map::DefaultHasher::new();
673            value.hash(&mut state);
674            state.finish()
675        }
676        for error in [
677            super::ExcelError::new_div(),
678            super::ExcelError::new_value().with_message("explanation"),
679            super::ExcelError::new(ExcelErrorKind::Spill)
680                .with_message("blocked")
681                .with_location(2, 3)
682                .with_origin(Some("Sheet1".into()), 4, 5)
683                .with_extra(ExcelErrorExtra::Spill {
684                    expected_rows: 6,
685                    expected_cols: 7,
686                }),
687        ] {
688            let old = ExcelError {
689                kind: error.kind,
690                message: error.message().map(str::to_owned),
691                context: error.context().cloned(),
692                extra: error.extra().clone(),
693            };
694            assert_eq!(format!("{error:?}"), format!("{old:?}"));
695            assert_eq!(hash(&error), hash(&old));
696            assert_eq!(error, error.clone());
697        }
698        let error = super::ExcelError::new(ExcelErrorKind::Spill)
699            .with_message("blocked")
700            .with_location(2, 3)
701            .with_origin(Some("Sheet1".into()), 4, 5)
702            .with_extra(ExcelErrorExtra::Spill {
703                expected_rows: 6,
704                expected_cols: 7,
705            });
706        assert_eq!(
707            error.to_string(),
708            "#SPILL!: blocked (row 2, col 3) [origin: Sheet1!R4C5] [spill 6×7]"
709        );
710        assert_ne!(error, error.clone().with_message("different"));
711        assert_ne!(error, error.clone().with_location(1, 1));
712        assert_ne!(error, error.clone().with_extra(ExcelErrorExtra::None));
713    }
714}