Skip to main content

visi_core/core/engine/
result_data.rs

1use serde::{Deserialize, Serialize};
2
3/// What a cell holds once it has been evaluated.
4///
5/// There is deliberately **no date variant**. As in Excel, a date is a plain
6/// numeric serial and the notation it was typed in lives on the cell, as
7/// `CellStyle::num_format` -- so `ISNUMBER` is true for a date, `SUM` counts
8/// it, and every numeric path works on it untouched. Only rendering consults
9/// the format, through `Sheet::get_display_string`.
10///
11/// An Excel error is a *value*, not a Rust error: `=1/0` evaluates
12/// successfully to `Error("#DIV/0!")`. See [`EngineError`] for the failures
13/// that are not values.
14///
15/// [`EngineError`]: crate::core::EngineError
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub enum ResultData {
18    /// A blank cell. Coerces to 0 or `""` depending on what reads it.
19    None,
20    /// `TRUE` or `FALSE`.
21    Boolean(bool),
22    /// A whole number.
23    Integer(i64),
24    /// A number that is not a whole number, or one too large for an `i64`.
25    /// A date is a `Float` holding its Excel serial.
26    Float(f64),
27    /// Text.
28    String(String),
29    /// An ordered sequence, for the engine-specific functions that return one.
30    /// Not an Excel array.
31    List(Vec<ResultData>),
32    /// Key/value pairs, for the engine-specific functions that return them.
33    Dict(Vec<(ResultData, ResultData)>),
34    /// An Excel error value, held as its code: `#DIV/0!`, `#VALUE!`, `#N/A`.
35    Error(String),
36}
37
38impl std::fmt::Display for ResultData {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self {
41            ResultData::None => write!(f, ""),
42            ResultData::Boolean(b) => write!(f, "{}", if *b { "TRUE" } else { "FALSE" }),
43            ResultData::Integer(i) => write!(f, "{}", i),
44            ResultData::Float(fl) => write!(f, "{}", format_excel_number(*fl)),
45            ResultData::String(s) => write!(f, "{}", s),
46            ResultData::List(l) => {
47                let items: Vec<String> = l.iter().map(|i| i.to_string()).collect();
48                write!(f, "[{}]", items.join(", "))
49            }
50            ResultData::Dict(d) => {
51                let items: Vec<String> = d.iter().map(|(k, v)| format!("{}: {}", k, v)).collect();
52                write!(f, "{{ {} }}", items.join(", "))
53            }
54            ResultData::Error(e) => write!(f, "Error: {}", e),
55        }
56    }
57}
58
59/// Rounds a significant-digit string to `keep` digits, half away from zero,
60/// trimming the trailing zeros Excel does not display. Returns the digits
61/// and the (possibly incremented) exponent -- rounding 999 up to 100 shifts
62/// the decimal point.
63fn round_digits_half_up(digits: &str, exp: i32, keep: usize) -> (String, i32) {
64    if digits.len() <= keep {
65        return (digits.to_string(), exp);
66    }
67    let mut kept: Vec<u8> = digits.as_bytes()[..keep].to_vec();
68    let round_up = digits.as_bytes()[keep] >= b'5';
69    let mut exp = exp;
70    if round_up {
71        let mut i = keep;
72        loop {
73            if i == 0 {
74                kept.insert(0, b'1');
75                kept.pop();
76                exp += 1;
77                break;
78            }
79            i -= 1;
80            if kept[i] == b'9' {
81                kept[i] = b'0';
82            } else {
83                kept[i] += 1;
84                break;
85            }
86        }
87    }
88    let mut out = String::from_utf8(kept).expect("ascii digits");
89    while out.len() > 1 && out.ends_with('0') {
90        out.pop();
91    }
92    (out, exp)
93}
94
95/// The Excel error values, spelled exactly as a cell shows them.
96///
97/// A closed set: these are the only strings a cell can hold that are an error
98/// rather than text, which is what makes recognising one on entry safe.
99pub(crate) const EXCEL_ERROR_CODES: &[&str] = &[
100    "#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A", "#CALC!", "#SPILL!",
101];
102
103/// Whether a literal cell entry is one of Excel's error values.
104///
105/// Typing `#NUM!` into Excel produces the error, not the text -- measured,
106/// along with the same thing happening when VBA assigns the string through
107/// `Range.Value`. So `Sheet::commit` recognises one, and `xlsx::text_cell_src`
108/// quotes it on import for the same reason it quotes `TRUE` and `6/22/26`:
109/// a cell Excel told us is *text* has to survive the round trip as text.
110pub(crate) fn is_excel_error_code(src: &str) -> bool {
111    EXCEL_ERROR_CODES
112        .iter()
113        .any(|e| src.eq_ignore_ascii_case(e))
114}
115
116pub(crate) fn format_excel_number(f: f64) -> String {
117    if f == 0.0 {
118        return "0".to_string();
119    }
120    if f.is_nan() || f.is_infinite() {
121        return "#NUM!".to_string();
122    }
123
124    let sci = format!("{:.14e}", f);
125    let (mantissa, exp_str) = sci.split_once('e').expect("{:e} always emits an exponent");
126    let exp: i32 = exp_str.parse().expect("{:e} emits an integer exponent");
127    let sign = if f < 0.0 { "-" } else { "" };
128    let digits: String = mantissa.chars().filter(|c| c.is_ascii_digit()).collect();
129    let digits = digits.trim_end_matches('0');
130    let digits = if digits.is_empty() { "0" } else { digits };
131
132    let decimal_len = if exp >= 0 {
133        let int_digits = (exp + 1) as usize;
134        let frac_digits = digits.len().saturating_sub(int_digits);
135        int_digits + usize::from(frac_digits > 0) + frac_digits
136    } else {
137        2 + (-exp - 1) as usize + digits.len()
138    };
139
140    if decimal_len <= 20 {
141        if exp >= 0 {
142            let int_digits = (exp + 1) as usize;
143            let mut out = String::from(sign);
144            if digits.len() <= int_digits {
145                out.push_str(digits);
146                out.push_str(&"0".repeat(int_digits - digits.len()));
147            } else {
148                out.push_str(&digits[..int_digits]);
149                out.push('.');
150                out.push_str(&digits[int_digits..]);
151            }
152            out
153        } else {
154            format!("{}0.{}{}", sign, "0".repeat((-exp - 1) as usize), digits)
155        }
156    } else {
157        let suffix_budget_len = if exp.abs() >= 99 {
158            5
159        } else {
160            format!("E{:+03}", exp).len()
161        };
162        let frac_digits = 18usize.saturating_sub(suffix_budget_len).min(14);
163
164        let (rounded_digits, exp) = round_digits_half_up(digits, exp, frac_digits + 1);
165        let mut mantissa = String::from(sign);
166        mantissa.push_str(&rounded_digits[..1]);
167        if rounded_digits.len() > 1 {
168            mantissa.push('.');
169            mantissa.push_str(&rounded_digits[1..]);
170        }
171        format!("{}E{:+03}", mantissa, exp)
172    }
173}