Skip to main content

spreadsheet_formulas/
evaluator.rs

1/// Evaluator — evaluates parsed formulas against a data context.
2
3use crate::ast::{BinOp, Expr, UnaryOp};
4use crate::builtins::eval_builtin;
5use std::collections::HashMap;
6
7/// A cell value in the spreadsheet.
8#[derive(Debug, Clone, PartialEq)]
9pub enum Value {
10    Number(f64),
11    Text(String),
12    Boolean(bool),
13    Array(Vec<Value>),
14    Null,
15    Error(String),
16}
17
18/// Data context: maps (row, col) to cell values.
19pub type DataContext = HashMap<(usize, usize), Value>;
20
21/// Evaluate an expression against a data context.
22pub fn evaluate(expr: &Expr, ctx: &DataContext) -> Result<Value, String> {
23    match expr {
24        Expr::Number(n) => Ok(Value::Number(*n)),
25        Expr::String(s) => Ok(Value::Text(s.clone())),
26        Expr::Boolean(b) => Ok(Value::Boolean(*b)),
27        Expr::CellRef(cell) => {
28            let key = (cell.row, cell.col);
29            Ok(ctx.get(&key).cloned().unwrap_or(Value::Number(0.0)))
30        }
31        Expr::Range(range) => {
32            let values: Vec<Value> = range
33                .iter_cells()
34                .filter_map(|(r, c)| ctx.get(&(r, c)).cloned())
35                .collect();
36            Ok(Value::Array(values))
37        }
38        Expr::BinaryOp { op, left, right } => {
39            let lval = evaluate(left, ctx)?;
40            let rval = evaluate(right, ctx)?;
41            eval_binary(*op, lval, rval)
42        }
43        Expr::UnaryOp { op: UnaryOp::Negate, operand } => {
44            let val = evaluate(operand, ctx)?;
45            match val {
46                Value::Number(n) => Ok(Value::Number(-n)),
47                _ => Err("Cannot negate non-number".into()),
48            }
49        }
50        Expr::Percent(expr) => {
51            let val = evaluate(expr, ctx)?;
52            match val {
53                Value::Number(n) => Ok(Value::Number(n / 100.0)),
54                _ => Err("Cannot apply % to non-number".into()),
55            }
56        }
57        Expr::FunctionCall { name, args } => {
58            let evaluated: Result<Vec<Value>, String> = args
59                .iter()
60                .map(|a| evaluate(a, ctx))
61                .collect();
62            eval_builtin(name, &evaluated?)
63        }
64    }
65}
66
67fn to_number(v: &Value) -> Result<f64, String> {
68    match v {
69        Value::Number(n) => Ok(*n),
70        Value::Boolean(b) => Ok(if *b { 1.0 } else { 0.0 }),
71        Value::Null => Ok(0.0),
72        Value::Text(s) => s.parse::<f64>().map_err(|_| format!("Cannot convert '{}' to number", s)),
73        Value::Error(e) => Err(e.clone()),
74        Value::Array(_) => Err("Cannot convert array to number".into()),
75    }
76}
77
78fn eval_binary(op: BinOp, left: Value, right: Value) -> Result<Value, String> {
79    match op {
80        BinOp::Add => {
81            let l = to_number(&left)?;
82            let r = to_number(&right)?;
83            Ok(Value::Number(l + r))
84        }
85        BinOp::Sub => {
86            let l = to_number(&left)?;
87            let r = to_number(&right)?;
88            Ok(Value::Number(l - r))
89        }
90        BinOp::Mul => {
91            let l = to_number(&left)?;
92            let r = to_number(&right)?;
93            Ok(Value::Number(l * r))
94        }
95        BinOp::Div => {
96            let l = to_number(&left)?;
97            let r = to_number(&right)?;
98            if r == 0.0 {
99                return Err("Division by zero".into());
100            }
101            Ok(Value::Number(l / r))
102        }
103        BinOp::Pow => {
104            let l = to_number(&left)?;
105            let r = to_number(&right)?;
106            Ok(Value::Number(l.powf(r)))
107        }
108        BinOp::Concat => {
109            let ls = value_to_string(&left);
110            let rs = value_to_string(&right);
111            Ok(Value::Text(format!("{}{}", ls, rs)))
112        }
113        BinOp::Eq => Ok(Value::Boolean(values_eq(&left, &right))),
114        BinOp::Neq => Ok(Value::Boolean(!values_eq(&left, &right))),
115        BinOp::Lt => {
116            let l = to_number(&left)?;
117            let r = to_number(&right)?;
118            Ok(Value::Boolean(l < r))
119        }
120        BinOp::Gt => {
121            let l = to_number(&left)?;
122            let r = to_number(&right)?;
123            Ok(Value::Boolean(l > r))
124        }
125        BinOp::Lte => {
126            let l = to_number(&left)?;
127            let r = to_number(&right)?;
128            Ok(Value::Boolean(l <= r))
129        }
130        BinOp::Gte => {
131            let l = to_number(&left)?;
132            let r = to_number(&right)?;
133            Ok(Value::Boolean(l >= r))
134        }
135    }
136}
137
138fn values_eq(a: &Value, b: &Value) -> bool {
139    match (a, b) {
140        (Value::Number(x), Value::Number(y)) => (x - y).abs() < f64::EPSILON,
141        (Value::Text(x), Value::Text(y)) => x == y,
142        (Value::Boolean(x), Value::Boolean(y)) => x == y,
143        (Value::Null, Value::Null) => true,
144        _ => false,
145    }
146}
147
148fn value_to_string(v: &Value) -> String {
149    match v {
150        Value::Number(n) => format!("{}", n),
151        Value::Text(s) => s.clone(),
152        Value::Boolean(b) => if *b { "TRUE".into() } else { "FALSE".into() },
153        Value::Null => String::new(),
154        Value::Error(e) => format!("#ERR: {}", e),
155        Value::Array(arr) => {
156            let parts: Vec<String> = arr.iter().map(value_to_string).collect();
157            format!("[{}]", parts.join(", "))
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::parser::Parser;
166
167    fn eval_str(formula: &str, ctx: &DataContext) -> Result<Value, String> {
168        let expr = Parser::parse_formula(formula)?;
169        evaluate(&expr, ctx)
170    }
171
172    #[test]
173    fn eval_arithmetic() {
174        let ctx = DataContext::new();
175        assert_eq!(eval_str("1+2*3", &ctx).unwrap(), Value::Number(7.0));
176    }
177
178    #[test]
179    fn eval_cell_ref() {
180        let mut ctx = DataContext::new();
181        ctx.insert((0, 0), Value::Number(42.0)); // A1
182        assert_eq!(eval_str("A1", &ctx).unwrap(), Value::Number(42.0));
183    }
184
185    #[test]
186    fn eval_range_sum() {
187        let mut ctx = DataContext::new();
188        for i in 0..5 {
189            ctx.insert((i, 0), Value::Number((i + 1) as f64)); // A1:A5
190        }
191        assert_eq!(eval_str("SUM(A1:A5)", &ctx).unwrap(), Value::Number(15.0));
192    }
193
194    #[test]
195    fn eval_division_by_zero() {
196        let ctx = DataContext::new();
197        assert!(eval_str("1/0", &ctx).is_err());
198    }
199
200    #[test]
201    fn eval_concat() {
202        let ctx = DataContext::new();
203        assert_eq!(eval_str("\"hello\"&\" world\"", &ctx).unwrap(), Value::Text("hello world".into()));
204    }
205
206    #[test]
207    fn eval_negate() {
208        let ctx = DataContext::new();
209        assert_eq!(eval_str("-(3+2)", &ctx).unwrap(), Value::Number(-5.0));
210    }
211
212    #[test]
213    fn eval_comparison() {
214        let ctx = DataContext::new();
215        assert_eq!(eval_str("5>3", &ctx).unwrap(), Value::Boolean(true));
216        assert_eq!(eval_str("2=2", &ctx).unwrap(), Value::Boolean(true));
217    }
218
219    #[test]
220    fn eval_percent() {
221        let ctx = DataContext::new();
222        assert_eq!(eval_str("50%", &ctx).unwrap(), Value::Number(0.5));
223    }
224
225    #[test]
226    fn eval_evolve() {
227        let mut ctx = DataContext::new();
228        for i in 0..10 {
229            ctx.insert((i, 0), Value::Number(i as f64));
230        }
231        let result = eval_str("EVOLVE(A1:A10, 100)", &ctx).unwrap();
232        assert!(matches!(result, Value::Number(_)));
233    }
234
235    #[test]
236    fn eval_entropy() {
237        let mut ctx = DataContext::new();
238        for i in 0..20 {
239            ctx.insert((i, 2), Value::Number((i % 4) as f64));
240        }
241        let result = eval_str("ENTROPY(C1:C20)", &ctx).unwrap();
242        assert!(matches!(result, Value::Number(_)));
243    }
244
245    #[test]
246    fn eval_power() {
247        let ctx = DataContext::new();
248        assert_eq!(eval_str("2^10", &ctx).unwrap(), Value::Number(1024.0));
249    }
250}