use std::collections::HashMap;
use serde_json::Value as JsonValue;
use crate::excel_error::ExcelError;
use crate::formula::Expr;
use crate::range_ref::cell_key;
use super::value::CellValue;
#[derive(Debug, Default, Clone)]
pub struct CellEnv {
values: HashMap<String, JsonValue>,
}
impl CellEnv {
pub fn new() -> Self {
Self {
values: HashMap::new(),
}
}
pub fn with_value(mut self, key: impl Into<String>, value: JsonValue) -> Self {
self.values.insert(key.into(), value);
self
}
pub fn seed_cell(mut self, key: impl Into<String>, value: &CellValue) -> Self {
if let Some(j) = to_json(value) {
self.values.insert(key.into(), j);
}
self
}
pub fn get(&self, key: &str) -> Option<&JsonValue> {
self.values.get(key)
}
}
pub fn to_json(v: &CellValue) -> Option<JsonValue> {
match v {
CellValue::Number(n) => serde_json::Number::from_f64(*n).map(JsonValue::Number),
CellValue::Bool(b) => Some(JsonValue::Bool(*b)),
CellValue::Text(s) => Some(JsonValue::String(s.clone())),
CellValue::Empty => Some(JsonValue::Number(0.into())), CellValue::Error(_) => None, }
}
pub fn from_json(v: &JsonValue) -> CellValue {
match v {
JsonValue::Number(n) => match n.as_f64() {
Some(f) if f.is_finite() => CellValue::Number(f),
Some(f) if f.is_nan() => CellValue::Error(ExcelError::DivZero),
_ => CellValue::Error(ExcelError::Num),
},
JsonValue::Bool(b) => CellValue::Bool(*b),
JsonValue::String(s) => CellValue::Text(s.clone()),
JsonValue::Null => CellValue::Empty,
JsonValue::Array(_) | JsonValue::Object(_) => CellValue::Error(ExcelError::Value),
}
}
pub fn preflight_error(expr: &Expr, errors: &HashMap<String, ExcelError>) -> Option<ExcelError> {
match expr {
Expr::Ref(addr) => errors.get(addr).copied(),
Expr::ErrorLit(e) => Some(*e),
Expr::Range(_) | Expr::Name(_) | Expr::Number(_) | Expr::Str(_) | Expr::Bool(_) => None,
Expr::BinaryOp { left, right, .. } => {
preflight_error(left, errors).or_else(|| preflight_error(right, errors))
},
Expr::UnaryOp { operand, .. } => preflight_error(operand, errors),
Expr::Call { args, .. } => args.iter().find_map(|a| preflight_error(a, errors)),
}
}
pub fn powf(base: f64, exp: f64) -> f64 {
base.powf(exp)
}
pub fn percent(x: f64) -> f64 {
x / 100.0
}
pub fn eval_leaf(expr: &Expr, env: &CellEnv, errors: &HashMap<String, ExcelError>) -> CellValue {
crate::scalar_eval::eval_scalar(expr, env, errors)
}
pub fn env_key(sheet: &str, addr: &str) -> String {
cell_key(sheet, addr)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::formula::{BinOp, UnOp};
use crate::range_ref::RangeRef;
fn num(n: f64) -> JsonValue {
JsonValue::Number(serde_json::Number::from_f64(n).unwrap())
}
#[test]
fn to_json_empty_is_zero_and_error_is_none() {
assert_eq!(
to_json(&CellValue::Empty),
Some(JsonValue::Number(0.into()))
);
assert_eq!(to_json(&CellValue::Error(ExcelError::Ref)), None);
assert_eq!(to_json(&CellValue::Number(2.5)), Some(num(2.5)));
assert_eq!(to_json(&CellValue::Bool(true)), Some(JsonValue::Bool(true)));
assert_eq!(
to_json(&CellValue::Text("x".into())),
Some(JsonValue::String("x".into()))
);
}
#[test]
fn from_json_maps_null_to_empty_and_array_to_value() {
assert_eq!(from_json(&num(3.0)), CellValue::Number(3.0));
assert_eq!(from_json(&JsonValue::Null), CellValue::Empty);
assert_eq!(from_json(&JsonValue::Bool(false)), CellValue::Bool(false));
assert_eq!(
from_json(&JsonValue::String("hi".into())),
CellValue::Text("hi".into())
);
assert_eq!(
from_json(&JsonValue::Array(vec![])),
CellValue::Error(ExcelError::Value)
);
}
#[test]
fn coil_band_leaf_arithmetic_via_pure_rust() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Ref("5_Quantities!C6".into())),
op: BinOp::Mul,
right: Box::new(Expr::Ref("2_Constants!C17".into())),
}),
op: BinOp::Add,
right: Box::new(Expr::Ref("2_Constants!C18".into())),
};
let env = CellEnv::new()
.with_value("5_Quantities!C6", num(10.0))
.with_value("2_Constants!C17", num(1.05))
.with_value("2_Constants!C18", num(50.0));
assert_eq!(
eval_leaf(&expr, &env, &HashMap::new()),
CellValue::Number(60.5)
);
}
#[test]
fn concat_via_pure_rust() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Str("a".into())),
op: BinOp::Concat,
right: Box::new(Expr::Str("b".into())),
};
assert_eq!(
eval_leaf(&expr, &CellEnv::new(), &HashMap::new()),
CellValue::Text("ab".into())
);
}
#[test]
fn comparison_le_returns_bool() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Number(2.0)),
op: BinOp::Le,
right: Box::new(Expr::Number(3.0)),
};
assert_eq!(
eval_leaf(&expr, &CellEnv::new(), &HashMap::new()),
CellValue::Bool(true)
);
}
#[test]
fn pow_is_computed_in_f64_not_lowered() {
assert_eq!(powf(2.0, 3.0), 8.0);
}
#[test]
fn percent_is_computed_in_f64() {
assert_eq!(percent(50.0), 0.5);
}
#[test]
fn preflight_error_returns_some_for_an_error_leaf_and_none_otherwise() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Ref("S!A1".into())),
op: BinOp::Add,
right: Box::new(Expr::Number(1.0)),
};
let mut errors = HashMap::new();
assert_eq!(preflight_error(&expr, &errors), None);
errors.insert("S!A1".to_string(), ExcelError::Ref);
assert_eq!(preflight_error(&expr, &errors), Some(ExcelError::Ref));
}
#[test]
fn eval_leaf_short_circuits_an_error_leaf_above_eval() {
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_leaf(&expr, &env, &errors);
assert_eq!(result, CellValue::Error(ExcelError::Na));
assert_ne!(result, CellValue::Number(101.0));
}
#[test]
fn error_lit_is_caught_by_preflight() {
let expr = Expr::ErrorLit(ExcelError::DivZero);
assert_eq!(
eval_leaf(&expr, &CellEnv::new(), &HashMap::new()),
CellValue::Error(ExcelError::DivZero)
);
}
#[test]
fn range_and_name_do_not_lower() {
let r = Expr::Range(RangeRef {
sheet: "S".into(),
start: "A1".into(),
end: "A3".into(),
});
assert_eq!(
eval_leaf(&r, &CellEnv::new(), &HashMap::new()),
CellValue::Error(ExcelError::Value)
);
assert_eq!(
eval_leaf(&Expr::Name("foo".into()), &CellEnv::new(), &HashMap::new()),
CellValue::Error(ExcelError::Value)
);
let _ = UnOp::Neg; }
#[test]
fn env_key_reuses_cell_key() {
assert_eq!(env_key("2_Constants", "C17"), "2_Constants!C17");
}
}