Skip to main content

rust_rule_engine/
expression.rs

1//! Expression Evaluator
2//!
3//! This module provides runtime evaluation of arithmetic expressions
4//! similar to CLIPS (bind ?total (* ?quantity ?price))
5
6use crate::engine::facts::Facts;
7use crate::errors::{Result, RuleEngineError};
8use crate::types::Value;
9
10/// Evaluate an arithmetic expression with field references
11/// Example: "Order.quantity * Order.price" with facts containing Order.quantity=10, Order.price=100
12/// Returns: Value::Integer(1000) or Value::Number(1000.0)
13pub fn evaluate_expression(expr: &str, facts: &Facts) -> Result<Value> {
14    let expr = expr.trim();
15
16    // Try to evaluate as simple arithmetic expression
17    // Support: +, -, *, /, %
18
19    // Find the operator (right to left for correct precedence)
20    // Precedence: *, /, % (higher) then +, - (lower)
21
22    // First pass: look for + or - (lowest precedence)
23    if let Some(pos) = find_operator(expr, &['+', '-']) {
24        let left = &expr[..pos].trim();
25        let op = &expr[pos..pos + 1];
26        let right = &expr[pos + 1..].trim();
27
28        let left_val = evaluate_expression(left, facts)?;
29        let right_val = evaluate_expression(right, facts)?;
30
31        return apply_operator(&left_val, op, &right_val);
32    }
33
34    // Second pass: look for *, /, % (higher precedence)
35    if let Some(pos) = find_operator(expr, &['*', '/', '%']) {
36        let left = &expr[..pos].trim();
37        let op = &expr[pos..pos + 1];
38        let right = &expr[pos + 1..].trim();
39
40        let left_val = evaluate_expression(left, facts)?;
41        let right_val = evaluate_expression(right, facts)?;
42
43        return apply_operator(&left_val, op, &right_val);
44    }
45
46    // No operator found - must be a single value
47    // Could be: string literal, field reference (Order.quantity), number (100), or variable
48
49    // Is it a string literal?
50    if expr.len() >= 2 {
51        let unquoted = &expr[1..expr.len() - 1];
52        if (expr.starts_with('"') && expr.ends_with('"') && !unquoted.contains('"'))
53            || (expr.starts_with('\'') && expr.ends_with('\'') && !unquoted.contains('\''))
54        {
55            let unquoted = &expr[1..expr.len() - 1];
56            return Ok(Value::String(unquoted.to_string()));
57        }
58    }
59
60    // Try to parse as number
61    if let Ok(int_val) = expr.parse::<i64>() {
62        return Ok(Value::Integer(int_val));
63    }
64
65    if let Ok(float_val) = expr.parse::<f64>() {
66        return Ok(Value::Number(float_val));
67    }
68
69    // Must be a field reference - try an exact (flat) key first, then a nested
70    // object path (e.g. "User.firstName" inside a Value::Object fact named "User")
71    if let Some(value) = facts.get(expr).or_else(|| facts.get_nested(expr)) {
72        return Ok(value);
73    }
74
75    // Field not found - return error
76    Err(RuleEngineError::EvaluationError {
77        message: format!("Field '{}' not found in facts", expr),
78    })
79}
80
81/// Find position of operator, skipping parentheses
82/// Returns rightmost occurrence for left-to-right evaluation
83fn find_operator(expr: &str, operators: &[char]) -> Option<usize> {
84    let mut paren_depth = 0;
85    let mut last_pos = None;
86
87    for (i, ch) in expr.chars().enumerate() {
88        match ch {
89            '(' => paren_depth += 1,
90            ')' => paren_depth -= 1,
91            _ if paren_depth == 0 && operators.contains(&ch) => {
92                last_pos = Some(i);
93            }
94            _ => {}
95        }
96    }
97
98    last_pos
99}
100
101/// Apply arithmetic operator to two values
102fn apply_operator(left: &Value, op: &str, right: &Value) -> Result<Value> {
103    // Convert to numbers
104    let left_num = value_to_number(left);
105    let right_num = value_to_number(right);
106
107    // The + sign can also mean string concatenation
108    if op == "+" && (left_num.is_err() || right_num.is_err()) {
109        // at least one operand cannoy be converted to numeric
110        let concatenated = match (left, right) {
111            // both operands are strings => concatenate
112            (Value::String(s1), Value::String(s2)) => format!("{}{}", s1, s2),
113            // at least one operand is not a string => error
114            _ => {
115                return Err(RuleEngineError::EvaluationError {
116                    message: "Only strings can be concatenated".to_string(),
117                })
118            }
119        };
120        return Ok(Value::String(concatenated));
121    }
122
123    let left_num = left_num.unwrap();
124    let right_num = right_num.unwrap();
125
126    let result = match op {
127        "+" => left_num + right_num,
128        "-" => left_num - right_num,
129        "*" => left_num * right_num,
130        "/" => {
131            if right_num == 0.0 {
132                return Err(RuleEngineError::EvaluationError {
133                    message: "Division by zero".to_string(),
134                });
135            }
136            left_num / right_num
137        }
138        "%" => left_num % right_num,
139        _ => {
140            return Err(RuleEngineError::EvaluationError {
141                message: format!("Unknown operator: {}", op),
142            });
143        }
144    };
145
146    // Return integer if both operands were integers and result is whole number
147    if is_integer_value(left) && is_integer_value(right) && result.fract() == 0.0 {
148        Ok(Value::Integer(result as i64))
149    } else {
150        Ok(Value::Number(result))
151    }
152}
153
154/// Convert Value to f64 for arithmetic
155fn value_to_number(value: &Value) -> Result<f64> {
156    match value {
157        Value::Integer(i) => Ok(*i as f64),
158        Value::Number(n) => Ok(*n),
159        Value::String(s) => s
160            .parse::<f64>()
161            .map_err(|_| RuleEngineError::EvaluationError {
162                message: format!("Cannot convert '{}' to number", s),
163            }),
164        _ => Err(RuleEngineError::EvaluationError {
165            message: format!("Cannot convert {:?} to number", value),
166        }),
167    }
168}
169
170/// Check if Value represents an integer
171fn is_integer_value(value: &Value) -> bool {
172    matches!(value, Value::Integer(_))
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_simple_arithmetic() {
181        let facts = Facts::new();
182
183        assert_eq!(
184            evaluate_expression("10 + 20", &facts).unwrap(),
185            Value::Integer(30)
186        );
187
188        assert_eq!(
189            evaluate_expression("100 - 25", &facts).unwrap(),
190            Value::Integer(75)
191        );
192
193        assert_eq!(
194            evaluate_expression("5 * 6", &facts).unwrap(),
195            Value::Integer(30)
196        );
197
198        assert_eq!(
199            evaluate_expression("100 / 4", &facts).unwrap(),
200            Value::Integer(25)
201        );
202    }
203
204    #[test]
205    fn test_field_references() {
206        let facts = Facts::new();
207        facts.set("Order.quantity", Value::Integer(10));
208        facts.set("Order.price", Value::Integer(100));
209
210        assert_eq!(
211            evaluate_expression("Order.quantity * Order.price", &facts).unwrap(),
212            Value::Integer(1000)
213        );
214    }
215
216    #[test]
217    fn test_mixed_operations() {
218        let facts = Facts::new();
219        facts.set("a", Value::Integer(10));
220        facts.set("b", Value::Integer(5));
221        facts.set("c", Value::Integer(2));
222
223        // 10 + 5 * 2 = 10 + 10 = 20
224        assert_eq!(
225            evaluate_expression("a + b * c", &facts).unwrap(),
226            Value::Integer(20)
227        );
228    }
229}