use crate::core::field_value::{DataRow, FieldValue};
use evalexpr::{ContextWithMutableVariables, HashMapContext, Value as EvalValue};
pub fn eval_formula(formula: &str, row: &DataRow) -> Result<FieldValue, String> {
let mut context = HashMapContext::new();
for (col_name, field_value) in row {
let eval_val = field_value_to_eval(field_value);
context
.set_value(col_name.clone(), eval_val)
.map_err(|e| format!("Failed to set variable '{}': {}", col_name, e))?;
}
let result = evalexpr::eval_with_context(formula, &context)
.map_err(|e| format!("Formula evaluation error: {}", e))?;
eval_to_field_value(&result)
}
fn field_value_to_eval(field: &FieldValue) -> EvalValue {
match field {
FieldValue::Numeric(n) => EvalValue::Float(*n),
FieldValue::Timestamp(t) => EvalValue::Float(*t),
FieldValue::Text(s) => EvalValue::String(s.clone()),
FieldValue::Bool(b) => EvalValue::Boolean(*b),
FieldValue::Null => EvalValue::Empty,
}
}
fn eval_to_field_value(val: &EvalValue) -> Result<FieldValue, String> {
match val {
EvalValue::Float(f) => Ok(FieldValue::Numeric(*f)),
EvalValue::Int(i) => Ok(FieldValue::Numeric(*i as f64)),
EvalValue::String(s) => Ok(FieldValue::Text(s.clone())),
EvalValue::Boolean(b) => Ok(FieldValue::Bool(*b)),
EvalValue::Empty => Ok(FieldValue::Null),
EvalValue::Tuple(_) => Err("Tuple results are not supported".to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn sample_row() -> DataRow {
let mut row = HashMap::new();
row.insert("price".to_string(), FieldValue::Numeric(100.0));
row.insert("quantity".to_string(), FieldValue::Numeric(5.0));
row.insert("tax_rate".to_string(), FieldValue::Numeric(0.2));
row.insert("category".to_string(), FieldValue::Text("A".to_string()));
row.insert("active".to_string(), FieldValue::Bool(true));
row
}
#[test]
fn test_arithmetic() {
let row = sample_row();
let result = eval_formula("price * quantity", &row).unwrap();
assert_eq!(result, FieldValue::Numeric(500.0));
let result = eval_formula("price * quantity * (1 + tax_rate)", &row).unwrap();
assert_eq!(result, FieldValue::Numeric(600.0));
let result = eval_formula("price / 2", &row).unwrap();
assert_eq!(result, FieldValue::Numeric(50.0));
}
#[test]
fn test_comparison() {
let row = sample_row();
let result = eval_formula("quantity > 3", &row).unwrap();
assert_eq!(result, FieldValue::Bool(true));
let result = eval_formula("category == \"A\"", &row).unwrap();
assert_eq!(result, FieldValue::Bool(true));
let result = eval_formula("price != 99", &row).unwrap();
assert_eq!(result, FieldValue::Bool(true));
}
#[test]
fn test_logical() {
let row = sample_row();
let result = eval_formula("active && quantity > 3", &row).unwrap();
assert_eq!(result, FieldValue::Bool(true));
let result = eval_formula("quantity < 1 || active", &row).unwrap();
assert_eq!(result, FieldValue::Bool(true));
let result = eval_formula("!active", &row).unwrap();
assert_eq!(result, FieldValue::Bool(false));
}
#[test]
fn test_string_concat() {
let mut row = HashMap::new();
row.insert(
"first_name".to_string(),
FieldValue::Text("John".to_string()),
);
row.insert("last_name".to_string(), FieldValue::Text("Doe".to_string()));
let result = eval_formula("first_name + \" \" + last_name", &row).unwrap();
assert_eq!(result, FieldValue::Text("John Doe".to_string()));
}
#[test]
fn test_missing_column() {
let row = sample_row();
let result = eval_formula("nonexistent_column * 2", &row);
assert!(result.is_err());
assert!(result.unwrap_err().contains("evaluation error"));
}
#[test]
fn test_syntax_error() {
let row = sample_row();
let result = eval_formula("price * * quantity", &row);
assert!(result.is_err());
}
#[test]
fn test_type_coercion() {
let row = sample_row();
let result = eval_formula("if(active, 1, 0)", &row).unwrap();
assert_eq!(result, FieldValue::Numeric(1.0));
}
#[test]
fn test_conditional_if() {
let row = sample_row();
let result = eval_formula("if(quantity > 10, 100, 50)", &row).unwrap();
assert_eq!(result, FieldValue::Numeric(50.0));
let result = eval_formula("if(quantity > 10, \"Bulk\", \"Retail\")", &row).unwrap();
assert_eq!(result, FieldValue::Text("Retail".to_string()));
}
#[test]
fn test_complex_formula() {
let row = sample_row();
let result = eval_formula(
"price * quantity * (1 - if(quantity > 10, 0.1, 0)) * (1 + tax_rate)",
&row,
)
.unwrap();
assert_eq!(result, FieldValue::Numeric(600.0)); }
}