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 - get from facts
70    if let Some(value) = facts.get(expr) {
71        return Ok(value.clone());
72    }
73
74    // Field not found - return error
75    Err(RuleEngineError::EvaluationError {
76        message: format!("Field '{}' not found in facts", expr),
77    })
78}
79
80/// Find position of operator, skipping parentheses
81/// Returns rightmost occurrence for left-to-right evaluation
82fn find_operator(expr: &str, operators: &[char]) -> Option<usize> {
83    let mut paren_depth = 0;
84    let mut last_pos = None;
85
86    for (i, ch) in expr.chars().enumerate() {
87        match ch {
88            '(' => paren_depth += 1,
89            ')' => paren_depth -= 1,
90            _ if paren_depth == 0 && operators.contains(&ch) => {
91                last_pos = Some(i);
92            }
93            _ => {}
94        }
95    }
96
97    last_pos
98}
99
100/// Apply arithmetic operator to two values
101fn apply_operator(left: &Value, op: &str, right: &Value) -> Result<Value> {
102    // Convert to numbers
103    let left_num = value_to_number(left);
104    let right_num = value_to_number(right);
105
106    // The + sign can also mean string concatenation
107    if op == "+" && (left_num.is_err() || right_num.is_err()) {
108        // at least one operand cannoy be converted to numeric
109        let concatenated = match (left, right) {
110            // both operands are strings => concatenate
111            (Value::String(s1), Value::String(s2)) => format!("{}{}", s1, s2),
112            // at least one operand is not a string => error
113            _ => {
114                return Err(RuleEngineError::EvaluationError {
115                    message: "Only strings can be concatenated".to_string(),
116                })
117            }
118        };
119        return Ok(Value::String(concatenated));
120    }
121
122    let left_num = left_num.unwrap();
123    let right_num = right_num.unwrap();
124
125    let result = match op {
126        "+" => left_num + right_num,
127        "-" => left_num - right_num,
128        "*" => left_num * right_num,
129        "/" => {
130            if right_num == 0.0 {
131                return Err(RuleEngineError::EvaluationError {
132                    message: "Division by zero".to_string(),
133                });
134            }
135            left_num / right_num
136        }
137        "%" => left_num % right_num,
138        _ => {
139            return Err(RuleEngineError::EvaluationError {
140                message: format!("Unknown operator: {}", op),
141            });
142        }
143    };
144
145    // Return integer if both operands were integers and result is whole number
146    if is_integer_value(left) && is_integer_value(right) && result.fract() == 0.0 {
147        Ok(Value::Integer(result as i64))
148    } else {
149        Ok(Value::Number(result))
150    }
151}
152
153/// Convert Value to f64 for arithmetic
154fn value_to_number(value: &Value) -> Result<f64> {
155    match value {
156        Value::Integer(i) => Ok(*i as f64),
157        Value::Number(n) => Ok(*n),
158        Value::String(s) => s
159            .parse::<f64>()
160            .map_err(|_| RuleEngineError::EvaluationError {
161                message: format!("Cannot convert '{}' to number", s),
162            }),
163        _ => Err(RuleEngineError::EvaluationError {
164            message: format!("Cannot convert {:?} to number", value),
165        }),
166    }
167}
168
169/// Check if Value represents an integer
170fn is_integer_value(value: &Value) -> bool {
171    matches!(value, Value::Integer(_))
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_simple_arithmetic() {
180        let facts = Facts::new();
181
182        assert_eq!(
183            evaluate_expression("10 + 20", &facts).unwrap(),
184            Value::Integer(30)
185        );
186
187        assert_eq!(
188            evaluate_expression("100 - 25", &facts).unwrap(),
189            Value::Integer(75)
190        );
191
192        assert_eq!(
193            evaluate_expression("5 * 6", &facts).unwrap(),
194            Value::Integer(30)
195        );
196
197        assert_eq!(
198            evaluate_expression("100 / 4", &facts).unwrap(),
199            Value::Integer(25)
200        );
201    }
202
203    #[test]
204    fn test_field_references() {
205        let facts = Facts::new();
206        facts.set("Order.quantity", Value::Integer(10));
207        facts.set("Order.price", Value::Integer(100));
208
209        assert_eq!(
210            evaluate_expression("Order.quantity * Order.price", &facts).unwrap(),
211            Value::Integer(1000)
212        );
213    }
214
215    #[test]
216    fn test_mixed_operations() {
217        let facts = Facts::new();
218        facts.set("a", Value::Integer(10));
219        facts.set("b", Value::Integer(5));
220        facts.set("c", Value::Integer(2));
221
222        // 10 + 5 * 2 = 10 + 10 = 20
223        assert_eq!(
224            evaluate_expression("a + b * c", &facts).unwrap(),
225            Value::Integer(20)
226        );
227    }
228}