Skip to main content

truecalc_core/engine/
mod.rs

1use std::collections::HashMap;
2
3use crate::eval::functions::Registry;
4use crate::eval::{evaluate_expr, Context, EvalCtx};
5use crate::parser::{parse_formula, Expr};
6use crate::types::{ErrorKind, ParseError, Value};
7
8/// Which spreadsheet product's semantics the engine targets.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10enum Flavor {
11    Sheets,
12    Excel,
13}
14
15pub struct Engine {
16    flavor: Flavor,
17    registry: Registry,
18}
19
20impl Engine {
21    /// Engine targeting Google Sheets conformance.
22    pub fn sheets() -> Self {
23        Self { flavor: Flavor::Sheets, registry: Registry::new() }
24    }
25
26    /// Engine targeting Excel conformance.
27    ///
28    /// Excel evaluation semantics are not implemented yet (they land in a
29    /// later phase): [`Engine::evaluate`] returns
30    /// `Value::Error(ErrorKind::NA)` for every formula. [`Engine::parse`]
31    /// and [`Engine::validate`] work.
32    pub fn excel() -> Self {
33        Self { flavor: Flavor::Excel, registry: Registry::new() }
34    }
35
36    /// Deprecated alias for [`Engine::sheets`].
37    #[deprecated(note = "use Engine::sheets() — engine flavor is required; see ADR 2026-04-27")]
38    pub fn google_sheets() -> Self {
39        Self::sheets()
40    }
41
42    /// Parse a formula string into an expression tree.
43    ///
44    /// The formula may start with `=`. Returns a [`ParseError`] if the input
45    /// is not a valid formula.
46    pub fn parse(&self, formula: &str) -> Result<Expr, ParseError> {
47        parse_formula(formula)
48    }
49
50    /// Validate that a formula string is syntactically correct without
51    /// returning the AST.
52    pub fn validate(&self, formula: &str) -> Result<(), ParseError> {
53        self.parse(formula).map(|_| ())
54    }
55
56    pub fn evaluate(&self, formula: &str, variables: &HashMap<String, Value>) -> Value {
57        if self.flavor == Flavor::Excel {
58            // Excel evaluation semantics are not implemented yet.
59            return Value::Error(ErrorKind::NA);
60        }
61        match parse_formula(formula) {
62            Err(_) => Value::Error(ErrorKind::Value),
63            Ok(expr) => {
64                let ctx = Context::new(variables.clone());
65                let mut eval_ctx = EvalCtx::new(ctx, &self.registry);
66                first_of_array(evaluate_expr(&expr, &mut eval_ctx))
67            }
68        }
69    }
70}
71
72fn first_of_array(v: Value) -> Value {
73    match v {
74        Value::Array(elems) if !elems.is_empty() => {
75            first_of_array(elems.into_iter().next().unwrap())
76        }
77        other => other,
78    }
79}
80
81#[cfg(test)]
82mod tests;