use std::collections::HashMap;
use serde_json::Value as JsonValue;
use crate::excel_error::ExcelError;
use crate::formula::{BinOp, Expr, UnOp};
use crate::sheet_ir::eval_bridge::{from_json, preflight_error, CellEnv};
use crate::sheet_ir::value::CellValue;
pub fn eval_scalar(expr: &Expr, env: &CellEnv, errors: &HashMap<String, ExcelError>) -> CellValue {
if let Some(err) = preflight_error(expr, errors) {
return CellValue::Error(err);
}
match eval_json(expr, env) {
Ok(j) => from_json(&j),
Err(_) => CellValue::Error(ExcelError::Value),
}
}
#[derive(Debug)]
enum ScalarError {
UndefinedVariable,
NotLowerable,
}
fn eval_json(expr: &Expr, env: &CellEnv) -> Result<JsonValue, ScalarError> {
match expr {
Expr::Ref(name) => env
.get(name)
.cloned()
.or_else(|| (name == "undefined").then_some(JsonValue::Null))
.ok_or(ScalarError::UndefinedVariable),
Expr::Number(n) => serde_json::Number::from_f64(*n)
.map(JsonValue::Number)
.ok_or(ScalarError::NotLowerable),
Expr::Str(s) => Ok(JsonValue::String(s.clone())),
Expr::Bool(b) => Ok(JsonValue::Bool(*b)),
Expr::BinaryOp { left, op, right } => {
let l = eval_json(left, env)?;
let r = eval_json(right, env)?;
eval_binop(&l, *op, &r)
},
Expr::UnaryOp { op, operand } => {
let v = eval_json(operand, env)?;
eval_unop(*op, &v)
},
Expr::Range(_) | Expr::Name(_) | Expr::Call { .. } | Expr::ErrorLit(_) => {
Err(ScalarError::NotLowerable)
},
}
}
fn eval_binop(left: &JsonValue, op: BinOp, right: &JsonValue) -> Result<JsonValue, ScalarError> {
let v = match op {
BinOp::Add => add_values(left, right),
BinOp::Sub => numeric_op(left, right, |a, b| a - b),
BinOp::Mul => numeric_op(left, right, |a, b| a * b),
BinOp::Div => numeric_op(left, right, |a, b| if b != 0.0 { a / b } else { f64::NAN }),
BinOp::Concat => {
let l_str = json_to_string(left);
let r_str = json_to_string(right);
JsonValue::String(format!("{l_str}{r_str}"))
},
BinOp::Eq => JsonValue::Bool(json_equals(left, right)),
BinOp::Ne => JsonValue::Bool(!json_equals(left, right)),
BinOp::Lt => JsonValue::Bool(to_number(left) < to_number(right)),
BinOp::Gt => JsonValue::Bool(to_number(left) > to_number(right)),
BinOp::Le => JsonValue::Bool(to_number(left) <= to_number(right)),
BinOp::Ge => JsonValue::Bool(to_number(left) >= to_number(right)),
BinOp::Pow => return Err(ScalarError::NotLowerable),
};
Ok(v)
}
fn eval_unop(op: UnOp, value: &JsonValue) -> Result<JsonValue, ScalarError> {
let v = match op {
UnOp::Pos => {
let n = to_number(value);
serde_json::Number::from_f64(n)
.map(JsonValue::Number)
.unwrap_or(JsonValue::Null)
},
UnOp::Neg => {
let n = to_number(value);
JsonValue::Number(
serde_json::Number::from_f64(-n).unwrap_or_else(|| serde_json::Number::from(0)),
)
},
UnOp::Percent => return Err(ScalarError::NotLowerable),
};
Ok(v)
}
fn to_number(value: &JsonValue) -> f64 {
match value {
JsonValue::Null => 0.0,
JsonValue::Bool(b) => {
if *b {
1.0
} else {
0.0
}
},
JsonValue::Number(n) => n.as_f64().unwrap_or(f64::NAN),
JsonValue::String(s) => s.parse().unwrap_or(f64::NAN),
JsonValue::Array(_) | JsonValue::Object(_) => f64::NAN,
}
}
fn add_values(left: &JsonValue, right: &JsonValue) -> JsonValue {
if matches!(left, JsonValue::String(_)) || matches!(right, JsonValue::String(_)) {
let l_str = json_to_string(left);
let r_str = json_to_string(right);
return JsonValue::String(format!("{l_str}{r_str}"));
}
let l = to_number(left);
let r = to_number(right);
JsonValue::Number(
serde_json::Number::from_f64(l + r).unwrap_or_else(|| serde_json::Number::from(0)),
)
}
fn numeric_op<F>(left: &JsonValue, right: &JsonValue, op: F) -> JsonValue
where
F: Fn(f64, f64) -> f64,
{
let l = to_number(left);
let r = to_number(right);
let result = op(l, r);
JsonValue::Number(
serde_json::Number::from_f64(result).unwrap_or_else(|| serde_json::Number::from(0)),
)
}
fn json_equals(left: &JsonValue, right: &JsonValue) -> bool {
match (left, right) {
(JsonValue::Null, JsonValue::Null) => true,
(JsonValue::Bool(a), JsonValue::Bool(b)) => a == b,
(JsonValue::Number(a), JsonValue::Number(b)) => {
a.as_f64().unwrap_or(f64::NAN) == b.as_f64().unwrap_or(f64::NAN)
},
(JsonValue::String(a), JsonValue::String(b)) => a == b,
(JsonValue::Number(n), JsonValue::String(s))
| (JsonValue::String(s), JsonValue::Number(n)) => {
if let Ok(parsed) = s.parse::<f64>() {
n.as_f64().unwrap_or(f64::NAN) == parsed
} else {
false
}
},
_ => false,
}
}
fn json_to_string(value: &JsonValue) -> String {
match value {
JsonValue::Null => "null".to_string(),
JsonValue::Bool(b) => b.to_string(),
JsonValue::Number(n) => n.to_string(),
JsonValue::String(s) => s.clone(),
JsonValue::Array(_) => value.to_string(),
JsonValue::Object(_) => "[object Object]".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn num(n: f64) -> JsonValue {
JsonValue::Number(serde_json::Number::from_f64(n).unwrap())
}
#[test]
fn add_two_refs() {
let env = CellEnv::new()
.with_value("S!A1", num(10.0))
.with_value("S!A2", num(5.0));
let expr = Expr::BinaryOp {
left: Box::new(Expr::Ref("S!A1".into())),
op: BinOp::Add,
right: Box::new(Expr::Ref("S!A2".into())),
};
assert_eq!(
eval_scalar(&expr, &env, &HashMap::new()),
CellValue::Number(15.0)
);
}
#[test]
fn div_by_zero_matches_kernel_nan_clamped_to_zero() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Number(1.0)),
op: BinOp::Div,
right: Box::new(Expr::Number(0.0)),
};
assert_eq!(
eval_scalar(&expr, &CellEnv::new(), &HashMap::new()),
CellValue::Number(0.0)
);
}
#[test]
fn pow_does_not_lower() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Number(2.0)),
op: BinOp::Pow,
right: Box::new(Expr::Number(3.0)),
};
assert_eq!(
eval_scalar(&expr, &CellEnv::new(), &HashMap::new()),
CellValue::Error(ExcelError::Value)
);
}
#[test]
fn error_leaf_short_circuits_above_arithmetic() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Ref("S!A1".into())),
op: BinOp::Add,
right: Box::new(Expr::Number(1.0)),
};
let env = CellEnv::new().with_value("S!A1", num(100.0));
let mut errors = HashMap::new();
errors.insert("S!A1".to_string(), ExcelError::Na);
let result = eval_scalar(&expr, &env, &errors);
assert_eq!(result, CellValue::Error(ExcelError::Na));
assert_ne!(result, CellValue::Number(101.0));
}
use crate::sheet_ir::rounding::{excel_round, excel_roundup};
const TOL: f64 = 0.01;
#[test]
fn quirk_half_rounding_uses_excel_round_source_of_truth() {
assert_eq!(excel_round(1594.925, 2), 1594.93);
assert_eq!(excel_round(2.5, 0), 3.0);
}
#[test]
fn quirk_negative_rounding_sign_away_from_zero() {
assert_eq!(excel_round(-2.5, 0), -3.0);
assert_eq!(excel_roundup(-3.001, 2), -3.01);
}
#[test]
fn quirk_empty_cell_coerces_to_zero_in_additive_context() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Ref("undefined".into())),
op: BinOp::Add,
right: Box::new(Expr::Number(5.0)),
};
assert_eq!(
eval_scalar(&expr, &CellEnv::new(), &HashMap::new()),
CellValue::Number(5.0)
);
}
#[test]
fn quirk_error_propagates_through_arithmetic() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Ref("S!A1".into())),
op: BinOp::Mul,
right: Box::new(Expr::Number(3.0)),
};
let env = CellEnv::new().with_value("S!A1", num(7.0)); let mut errors = HashMap::new();
errors.insert("S!A1".to_string(), ExcelError::Na);
let result = eval_scalar(&expr, &env, &errors);
assert_eq!(result, CellValue::Error(ExcelError::Na));
assert_ne!(result, CellValue::Number(21.0));
}
#[test]
fn quirk_explicit_div_zero_error_propagates() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Ref("S!A1".into())),
op: BinOp::Add,
right: Box::new(Expr::Number(1.0)),
};
let env = CellEnv::new().with_value("S!A1", num(100.0));
let mut errors = HashMap::new();
errors.insert("S!A1".to_string(), ExcelError::DivZero);
let result = eval_scalar(&expr, &env, &errors);
assert_eq!(result, CellValue::Error(ExcelError::DivZero));
}
#[test]
fn quirk_text_to_number_coercion_is_context_specific() {
let mul = Expr::BinaryOp {
left: Box::new(Expr::Str("5.5".into())),
op: BinOp::Mul,
right: Box::new(Expr::Number(2.0)),
};
assert_eq!(
eval_scalar(&mul, &CellEnv::new(), &HashMap::new()),
CellValue::Number(11.0)
);
let add = Expr::BinaryOp {
left: Box::new(Expr::Str("5.5".into())),
op: BinOp::Add,
right: Box::new(Expr::Number(2.0)),
};
assert!(
matches!(
eval_scalar(&add, &CellEnv::new(), &HashMap::new()),
CellValue::Text(_)
),
"the additive `+` context concatenates a string operand (text), it does \
NOT arithmetically coerce it like the multiplicative `*` context"
);
}
#[test]
fn quirk_float_boundary_compares_within_tol_not_exact() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Number(0.1)),
op: BinOp::Add,
right: Box::new(Expr::Number(0.2)),
};
let result = eval_scalar(&expr, &CellEnv::new(), &HashMap::new());
let CellValue::Number(n) = result else {
panic!("expected a Number, got {result:?}");
};
assert_ne!(n, 0.3, "0.1+0.2 is not exactly 0.3 in binary-f64");
assert!((n - 0.3).abs() <= TOL, "{n} reconciles to 0.3 within TOL");
}
#[test]
fn quirk_1900_leap_serial_offset_components() {
let gt = Expr::BinaryOp {
left: Box::new(Expr::Number(61.0)),
op: BinOp::Gt,
right: Box::new(Expr::Number(59.0)),
};
assert_eq!(
eval_scalar(>, &CellEnv::new(), &HashMap::new()),
CellValue::Bool(true)
);
let offset = Expr::BinaryOp {
left: Box::new(Expr::Number(61.0)),
op: BinOp::Add,
right: Box::new(Expr::Number(1.0)),
};
assert_eq!(
eval_scalar(&offset, &CellEnv::new(), &HashMap::new()),
CellValue::Number(62.0)
);
let at_boundary = Expr::BinaryOp {
left: Box::new(Expr::Number(59.0)),
op: BinOp::Gt,
right: Box::new(Expr::Number(59.0)),
};
assert_eq!(
eval_scalar(&at_boundary, &CellEnv::new(), &HashMap::new()),
CellValue::Bool(false)
);
}
}