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/// We intentionally omit a date type. In Excel, a date is a plain
6/// numeric serial and the notation is saved separately, as
7/// `CellStyle::num_format`. This way, we can treat dates like
8/// any other numeric type (e.g. `SUM`).
9///
10/// An Excel error is a *value*, not a Rust error, e.g. `=1/0` evaluates
11/// successfully to `Error("#DIV/0!")`. See [`EngineError`] for Rust errors.
12///
13/// [`EngineError`]: crate::core::EngineError
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub enum ResultData {
16    /// A blank cell. Coerces to 0 or `""` depending on what reads it.
17    None,
18    /// `TRUE` or `FALSE`.
19    Boolean(bool),
20    /// A whole number.
21    Integer(i64),
22    /// A number that is not a whole number, or one too large for an `i64`.
23    /// A date is a `Float` holding its Excel serial.
24    Float(f64),
25    /// Text.
26    String(String),
27    /// An ordered sequence, for the engine-specific functions that return one.
28    /// Not an Excel array.
29    List(Vec<ResultData>),
30    /// Key/value pairs, for the engine-specific functions that return them.
31    Dict(Vec<(ResultData, ResultData)>),
32    /// An Excel error value, held as its code: `#DIV/0!`, `#VALUE!`, `#N/A`.
33    Error(String),
34}
35
36impl std::fmt::Display for ResultData {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            ResultData::None => write!(f, ""),
40            ResultData::Boolean(b) => write!(f, "{}", if *b { "TRUE" } else { "FALSE" }),
41            ResultData::Integer(i) => write!(f, "{}", i),
42            ResultData::Float(fl) => write!(f, "{}", format_excel_number(*fl)),
43            ResultData::String(s) => write!(f, "{}", s),
44            ResultData::List(l) => {
45                let items: Vec<String> = l.iter().map(|i| i.to_string()).collect();
46                write!(f, "[{}]", items.join(", "))
47            }
48            ResultData::Dict(d) => {
49                let items: Vec<String> = d.iter().map(|(k, v)| format!("{}: {}", k, v)).collect();
50                write!(f, "{{ {} }}", items.join(", "))
51            }
52            ResultData::Error(e) => write!(f, "Error: {}", e),
53        }
54    }
55}
56
57fn round_digits_half_up(digits: &str, exp: i32, keep: usize) -> (String, i32) {
58    if digits.len() <= keep {
59        return (digits.to_string(), exp);
60    }
61    let mut kept: Vec<u8> = digits.as_bytes()[..keep].to_vec();
62    let round_up = digits.as_bytes()[keep] >= b'5';
63    let mut exp = exp;
64    if round_up {
65        let mut i = keep;
66        loop {
67            if i == 0 {
68                kept.insert(0, b'1');
69                kept.pop();
70                exp += 1;
71                break;
72            }
73            i -= 1;
74            if kept[i] == b'9' {
75                kept[i] = b'0';
76            } else {
77                kept[i] += 1;
78                break;
79            }
80        }
81    }
82    let mut out = String::from_utf8(kept).expect("ascii digits");
83    while out.len() > 1 && out.ends_with('0') {
84        out.pop();
85    }
86    (out, exp)
87}
88
89pub(crate) const EXCEL_ERROR_CODES: &[&str] = &[
90    "#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A", "#CALC!", "#SPILL!",
91];
92
93pub(crate) fn is_excel_error_code(src: &str) -> bool {
94    EXCEL_ERROR_CODES
95        .iter()
96        .any(|e| src.eq_ignore_ascii_case(e))
97}
98
99pub(crate) fn format_excel_number(f: f64) -> String {
100    if f == 0.0 {
101        return "0".to_string();
102    }
103    if f.is_nan() || f.is_infinite() {
104        return "#NUM!".to_string();
105    }
106
107    let sci = format!("{:.14e}", f);
108    let (mantissa, exp_str) = sci.split_once('e').expect("{:e} always emits an exponent");
109    let exp: i32 = exp_str.parse().expect("{:e} emits an integer exponent");
110    let sign = if f < 0.0 { "-" } else { "" };
111    let digits: String = mantissa.chars().filter(|c| c.is_ascii_digit()).collect();
112    let digits = digits.trim_end_matches('0');
113    let digits = if digits.is_empty() { "0" } else { digits };
114
115    let decimal_len = if exp >= 0 {
116        let int_digits = (exp + 1) as usize;
117        let frac_digits = digits.len().saturating_sub(int_digits);
118        int_digits + usize::from(frac_digits > 0) + frac_digits
119    } else {
120        2 + (-exp - 1) as usize + digits.len()
121    };
122
123    if decimal_len <= 20 {
124        if exp >= 0 {
125            let int_digits = (exp + 1) as usize;
126            let mut out = String::from(sign);
127            if digits.len() <= int_digits {
128                out.push_str(digits);
129                out.push_str(&"0".repeat(int_digits - digits.len()));
130            } else {
131                out.push_str(&digits[..int_digits]);
132                out.push('.');
133                out.push_str(&digits[int_digits..]);
134            }
135            out
136        } else {
137            format!("{}0.{}{}", sign, "0".repeat((-exp - 1) as usize), digits)
138        }
139    } else {
140        let suffix_budget_len = if exp.abs() >= 99 {
141            5
142        } else {
143            format!("E{:+03}", exp).len()
144        };
145        let frac_digits = 18usize.saturating_sub(suffix_budget_len).min(14);
146
147        let (rounded_digits, exp) = round_digits_half_up(digits, exp, frac_digits + 1);
148        let mut mantissa = String::from(sign);
149        mantissa.push_str(&rounded_digits[..1]);
150        if rounded_digits.len() > 1 {
151            mantissa.push('.');
152            mantissa.push_str(&rounded_digits[1..]);
153        }
154        format!("{}E{:+03}", mantissa, exp)
155    }
156}