use crate::intrinsics::fold;
use crate::rows;
use crate::sheet::number_text;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ErrorKind {
Null,
Div0,
Value,
Ref,
Name,
Num,
NA,
}
impl ErrorKind {
pub fn text(self) -> &'static str {
match self {
ErrorKind::Null => "#NULL!",
ErrorKind::Div0 => "#DIV/0!",
ErrorKind::Value => "#VALUE!",
ErrorKind::Ref => "#REF!",
ErrorKind::Name => "#NAME?",
ErrorKind::Num => "#NUM!",
ErrorKind::NA => "#N/A",
}
}
pub fn parse(text: &str) -> Option<ErrorKind> {
match fold(text.trim()).as_str() {
"#null!" => Some(ErrorKind::Null),
"#div/0!" => Some(ErrorKind::Div0),
"#value!" => Some(ErrorKind::Value),
"#ref!" => Some(ErrorKind::Ref),
"#name?" => Some(ErrorKind::Name),
"#num!" => Some(ErrorKind::Num),
"#n/a" => Some(ErrorKind::NA),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Value {
Number(f64),
Text(String),
Bool(bool),
Error(ErrorKind),
Empty,
}
impl Value {
pub fn number(&self) -> Result<f64, ErrorKind> {
match self {
Value::Number(v) => Ok(*v),
Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
Value::Empty => Ok(0.0),
Value::Text(t) => text_to_number(t).ok_or(ErrorKind::Value),
Value::Error(k) => Err(*k),
}
}
pub fn text(&self) -> Result<String, ErrorKind> {
match self {
Value::Text(t) => Ok(t.clone()),
Value::Number(v) => Ok(number_to_text(*v)),
Value::Bool(true) => Ok("TRUE".to_string()),
Value::Bool(false) => Ok("FALSE".to_string()),
Value::Empty => Ok(String::new()),
Value::Error(k) => Err(*k),
}
}
pub fn truth(&self) -> Result<bool, ErrorKind> {
match self {
Value::Bool(b) => Ok(*b),
Value::Number(v) => Ok(*v != 0.0),
Value::Empty => Ok(false),
Value::Text(t) => match fold(t).as_str() {
"true" => Ok(true),
"false" => Ok(false),
_ => Err(ErrorKind::Value),
},
Value::Error(k) => Err(*k),
}
}
pub fn settled(self) -> Value {
match self {
Value::Empty => Value::Number(0.0),
other => other,
}
}
pub fn spell(&self) -> String {
match self {
Value::Number(v) if *v == 0.0 => "0".to_string(),
Value::Number(v) => match number_text(*v) {
Some(text) => text,
None => rows::datum(&rows::Value::Error(ErrorKind::Num.text().to_string())),
},
Value::Text(t) => rows::quoted(t),
Value::Bool(true) => "true".to_string(),
Value::Bool(false) => "false".to_string(),
Value::Error(k) => rows::datum(&rows::Value::Error(k.text().to_string())),
Value::Empty => "0".to_string(),
}
}
pub fn from_file(v: &rows::Value) -> Option<Value> {
match v {
rows::Value::Number(text) => {
let n: f64 = text.trim().parse().ok()?;
n.is_finite().then_some(Value::Number(n))
}
rows::Value::Text(t) => Some(Value::Text(t.clone())),
rows::Value::Bool(b) => Some(Value::Bool(*b)),
rows::Value::Error(text) => ErrorKind::parse(text).map(Value::Error),
rows::Value::Date(_) => None,
}
}
}
pub fn round15(v: f64) -> f64 {
if v == 0.0 || !v.is_finite() {
return v;
}
format!("{v:.14e}").parse().unwrap_or(v)
}
pub fn number_to_text(v: f64) -> String {
if v == 0.0 {
return "0".to_string();
}
let r = round15(v);
let sci = format!("{r:.14e}");
let (mantissa, exponent) = sci.split_once('e').unwrap_or((sci.as_str(), "0"));
let exponent: i32 = exponent.parse().unwrap_or(0);
if (-14..=14).contains(&exponent) {
return format!("{r}");
}
let mantissa = mantissa.trim_end_matches('0').trim_end_matches('.');
format!(
"{mantissa}E{}{:02}",
if exponent < 0 { '-' } else { '+' },
exponent.abs()
)
}
pub fn text_to_number(text: &str) -> Option<f64> {
let t = text.trim();
let (t, percent) = match t.strip_suffix('%') {
Some(rest) => (rest.trim_end(), true),
None => (t, false),
};
let bytes = t.as_bytes();
if bytes.is_empty() {
return None;
}
let mut i = 0;
if bytes[i] == b'+' || bytes[i] == b'-' {
i += 1;
}
let mut digits = 0;
let mut dot = false;
while i < bytes.len() {
match bytes[i] {
b'0'..=b'9' => digits += 1,
b'.' if !dot => dot = true,
_ => break,
}
i += 1;
}
if digits == 0 {
return None;
}
if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
let mut j = i + 1;
if j < bytes.len() && (bytes[j] == b'+' || bytes[j] == b'-') {
j += 1;
}
let first = j;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
if j == first {
return None;
}
i = j;
}
if i != bytes.len() {
return None;
}
let v: f64 = t.parse().ok()?;
let v = if percent { v / 100.0 } else { v };
v.is_finite().then_some(v)
}
pub fn agrees(computed: &Value, cached: &rows::Value) -> Option<bool> {
let cached = Value::from_file(cached)?;
let computed = computed.clone().settled();
Some(match (&computed, &cached) {
(Value::Number(a), Value::Number(b)) => {
let key = |x: f64| format!("{:.14e}", if x == 0.0 { 0.0 } else { round15(x) });
key(*a) == key(*b)
}
(Value::Text(a), Value::Text(b)) => a == b,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::Error(a), Value::Error(b)) => a == b,
_ => false,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_seven_errors_spell_and_parse() {
for k in [
ErrorKind::Null,
ErrorKind::Div0,
ErrorKind::Value,
ErrorKind::Ref,
ErrorKind::Name,
ErrorKind::Num,
ErrorKind::NA,
] {
assert_eq!(ErrorKind::parse(k.text()), Some(k));
assert_eq!(ErrorKind::parse(&k.text().to_lowercase()), Some(k));
}
assert_eq!(ErrorKind::parse("#SPILL!"), None);
}
#[test]
fn numbers_are_read_as_excel_reads_an_operand() {
assert_eq!(Value::Text("5".into()).number(), Ok(5.0));
assert_eq!(Value::Text(" 1e3 ".into()).number(), Ok(1000.0));
assert_eq!(Value::Text("50%".into()).number(), Ok(0.5));
assert_eq!(Value::Text("-.5".into()).number(), Ok(-0.5));
assert_eq!(Value::Text("abc".into()).number(), Err(ErrorKind::Value));
assert_eq!(Value::Text("".into()).number(), Err(ErrorKind::Value));
assert_eq!(Value::Text("1,000".into()).number(), Err(ErrorKind::Value));
assert_eq!(Value::Bool(true).number(), Ok(1.0));
assert_eq!(Value::Empty.number(), Ok(0.0));
assert_eq!(Value::Error(ErrorKind::NA).number(), Err(ErrorKind::NA));
}
#[test]
fn texts_are_excel_s_general_spelling() {
assert_eq!(number_to_text(15.0), "15");
assert_eq!(number_to_text(0.1 + 0.2), "0.3");
assert_eq!(number_to_text(1.0 / 3.0), "0.333333333333333");
assert_eq!(number_to_text(-2.5), "-2.5");
assert_eq!(number_to_text(0.000001), "0.000001");
assert_eq!(number_to_text(0.0000001), "0.0000001");
assert_eq!(number_to_text(1e20), "1E+20");
assert_eq!(number_to_text(1e21), "1E+21");
assert_eq!(number_to_text(1e14), "100000000000000");
assert_eq!(number_to_text(1e15), "1E+15");
assert_eq!(number_to_text(999_999_999_999_999.0), "999999999999999");
assert_eq!(number_to_text(999_999_999_999_999.9), "1E+15");
assert_eq!(number_to_text(2f64.powi(57)), "1.44115188075856E+17");
assert_eq!(number_to_text(1e-14), "0.00000000000001");
assert_eq!(number_to_text(1e-15), "1E-15");
assert_eq!(number_to_text(1.5e-7), "0.00000015");
assert_eq!(number_to_text(-0.0000001), "-0.0000001");
assert_eq!(
number_to_text(1.0 / 3.0 * 1e-10),
"0.0000000000333333333333333"
);
assert_eq!(number_to_text(2f64.sqrt()), "1.4142135623731");
assert_eq!(number_to_text(0.0), "0");
assert_eq!(Value::Bool(true).text(), Ok("TRUE".to_string()));
assert_eq!(Value::Empty.text(), Ok(String::new()));
}
#[test]
fn truth_is_read_from_numbers_and_the_two_words() {
assert_eq!(Value::Number(2.0).truth(), Ok(true));
assert_eq!(Value::Number(0.0).truth(), Ok(false));
assert_eq!(Value::Text("true".into()).truth(), Ok(true));
assert_eq!(Value::Text("yes".into()).truth(), Err(ErrorKind::Value));
assert_eq!(Value::Empty.truth(), Ok(false));
}
#[test]
fn a_value_spells_as_a_cell_row_does() {
assert_eq!(Value::Number(15.0).spell(), "15");
assert_eq!(Value::Number(0.6).spell(), "0.6");
assert_eq!(Value::Text("big".into()).spell(), "\"big\"");
assert_eq!(Value::Bool(false).spell(), "false");
assert_eq!(Value::Error(ErrorKind::Div0).spell(), "(error \"#DIV/0!\")");
assert_eq!(Value::Number(f64::INFINITY).spell(), "(error \"#NUM!\")");
assert_eq!(Value::Empty.spell(), "0");
assert_eq!(Value::Empty.settled(), Value::Number(0.0));
}
#[test]
fn agreement_carries_the_stated_tolerance_and_nothing_more() {
let cached = |s: &str| rows::Value::Number(s.to_string());
assert_eq!(
agrees(&Value::Number(0.1 + 0.2), &cached("0.3")),
Some(true)
);
assert_eq!(
agrees(&Value::Number(0.1 + 0.2), &cached("0.30000000000000004")),
Some(true)
);
assert_eq!(agrees(&Value::Number(15.0), &cached("15")), Some(true));
assert_eq!(
agrees(&Value::Number(15.0), &cached("15.000000000001")),
Some(false)
);
assert_eq!(agrees(&Value::Empty, &cached("0")), Some(true));
assert_eq!(
agrees(&Value::Text("big".into()), &rows::Value::Text("big".into())),
Some(true)
);
assert_eq!(
agrees(&Value::Text("big".into()), &rows::Value::Text("Big".into())),
Some(false)
);
assert_eq!(
agrees(
&Value::Error(ErrorKind::Div0),
&rows::Value::Error("#DIV/0!".into())
),
Some(true)
);
assert_eq!(
agrees(&Value::Number(1.0), &rows::Value::Date("2026-10-08".into())),
None
);
assert_eq!(
agrees(&Value::Number(1.0), &rows::Value::Bool(true)),
Some(false)
);
}
}