1use alloc::string::String;
11
12use crate::ast::{CmpOp, Expr, Operand, Value};
13
14pub trait RowAccess {
20 fn get(&self, path: &str) -> Option<Value>;
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum EvalError {
28 UnknownField(String),
30 MissingParam(u32),
32 TypeMismatch(String),
34}
35
36impl core::fmt::Display for EvalError {
37 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
38 match self {
39 Self::UnknownField(n) => write!(f, "unknown field: {n}"),
40 Self::MissingParam(i) => write!(f, "missing parameter %{i}"),
41 Self::TypeMismatch(m) => write!(f, "type mismatch: {m}"),
42 }
43 }
44}
45
46#[cfg(feature = "std")]
47impl std::error::Error for EvalError {}
48
49impl Expr {
50 pub fn evaluate<R: RowAccess>(&self, row: &R, params: &[Value]) -> Result<bool, EvalError> {
55 match self {
56 Self::And(a, b) => Ok(a.evaluate(row, params)? && b.evaluate(row, params)?),
57 Self::Or(a, b) => Ok(a.evaluate(row, params)? || b.evaluate(row, params)?),
58 Self::Not(inner) => Ok(!inner.evaluate(row, params)?),
59 Self::Cmp { lhs, op, rhs } => {
60 let l = resolve_operand(lhs, row, params)?;
61 let r = resolve_operand(rhs, row, params)?;
62 cmp(&l, *op, &r)
63 }
64 Self::Between {
65 field,
66 low,
67 high,
68 negated,
69 } => {
70 let f = resolve_operand(field, row, params)?;
71 let lo = resolve_operand(low, row, params)?;
72 let hi = resolve_operand(high, row, params)?;
73 let in_range = cmp(&f, CmpOp::Ge, &lo)? && cmp(&f, CmpOp::Le, &hi)?;
74 Ok(if *negated { !in_range } else { in_range })
75 }
76 }
77 }
78}
79
80fn resolve_operand<R: RowAccess>(
81 op: &Operand,
82 row: &R,
83 params: &[Value],
84) -> Result<Value, EvalError> {
85 match op {
86 Operand::Literal(v) => Ok(v.clone()),
87 Operand::Field(name) => row
88 .get(name)
89 .ok_or_else(|| EvalError::UnknownField(name.clone())),
90 Operand::Param(i) => params
91 .get(*i as usize)
92 .cloned()
93 .ok_or(EvalError::MissingParam(*i)),
94 }
95}
96
97fn cmp(lhs: &Value, op: CmpOp, rhs: &Value) -> Result<bool, EvalError> {
98 if let (Some(l), Some(r)) = (as_f64(lhs), as_f64(rhs)) {
100 return Ok(match op {
101 CmpOp::Eq => (l - r).abs() < f64::EPSILON,
102 CmpOp::Neq => (l - r).abs() >= f64::EPSILON,
103 CmpOp::Lt => l < r,
104 CmpOp::Le => l <= r,
105 CmpOp::Gt => l > r,
106 CmpOp::Ge => l >= r,
107 CmpOp::Like => {
108 return Err(EvalError::TypeMismatch("LIKE nur für String".into()));
109 }
110 });
111 }
112
113 match (lhs, rhs, op) {
114 (Value::String(a), Value::String(b), CmpOp::Eq) => Ok(a == b),
115 (Value::String(a), Value::String(b), CmpOp::Neq) => Ok(a != b),
116 (Value::String(a), Value::String(b), CmpOp::Lt) => Ok(a < b),
117 (Value::String(a), Value::String(b), CmpOp::Le) => Ok(a <= b),
118 (Value::String(a), Value::String(b), CmpOp::Gt) => Ok(a > b),
119 (Value::String(a), Value::String(b), CmpOp::Ge) => Ok(a >= b),
120 (Value::String(a), Value::String(b), CmpOp::Like) => Ok(like_match(a, b)),
121 (Value::Bool(a), Value::Bool(b), CmpOp::Eq) => Ok(a == b),
122 (Value::Bool(a), Value::Bool(b), CmpOp::Neq) => Ok(a != b),
123 (a, b, op) => Err(EvalError::TypeMismatch(alloc::format!(
124 "{a:?} {op:?} {b:?}"
125 ))),
126 }
127}
128
129fn as_f64(v: &Value) -> Option<f64> {
130 match v {
131 #[allow(clippy::cast_precision_loss)]
132 Value::Int(n) => Some(*n as f64),
133 Value::Float(f) => Some(*f),
134 _ => None,
135 }
136}
137
138fn like_match(s: &str, pat: &str) -> bool {
142 let s_chars: alloc::vec::Vec<char> = s.chars().collect();
144 let p_chars: alloc::vec::Vec<char> = pat.chars().collect();
145 let (m, n) = (s_chars.len(), p_chars.len());
146 let mut dp = alloc::vec![alloc::vec![false; n + 1]; m + 1];
147 dp[0][0] = true;
148 for j in 1..=n {
149 if p_chars[j - 1] == '%' {
150 dp[0][j] = dp[0][j - 1];
151 }
152 }
153 for i in 1..=m {
154 for j in 1..=n {
155 let pc = p_chars[j - 1];
156 dp[i][j] = if pc == '%' {
157 dp[i - 1][j] || dp[i][j - 1]
158 } else if pc == '_' || pc == s_chars[i - 1] {
159 dp[i - 1][j - 1]
160 } else {
161 false
162 };
163 }
164 }
165 dp[m][n]
166}
167
168#[cfg(test)]
169#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
170mod tests {
171 use super::*;
172 use crate::parser::parse;
173 use alloc::collections::BTreeMap;
174
175 struct MapRow(BTreeMap<String, Value>);
176 impl RowAccess for MapRow {
177 fn get(&self, path: &str) -> Option<Value> {
178 self.0.get(path).cloned()
179 }
180 }
181
182 fn row(pairs: &[(&str, Value)]) -> MapRow {
183 let mut m = BTreeMap::new();
184 for (k, v) in pairs {
185 m.insert((*k).into(), v.clone());
186 }
187 MapRow(m)
188 }
189
190 #[test]
191 fn evaluates_string_eq() {
192 let e = parse("color = 'RED'").unwrap();
193 let r = row(&[("color", Value::String("RED".into()))]);
194 assert_eq!(e.evaluate(&r, &[]), Ok(true));
195 }
196
197 #[test]
198 fn evaluates_int_compare() {
199 let e = parse("x > 10 AND x <= 100").unwrap();
200 let r = row(&[("x", Value::Int(42))]);
201 assert_eq!(e.evaluate(&r, &[]), Ok(true));
202 }
203
204 #[test]
205 fn evaluates_float_int_cross() {
206 let e = parse("x < 3.5").unwrap();
208 let r = row(&[("x", Value::Int(3))]);
209 assert_eq!(e.evaluate(&r, &[]), Ok(true));
210 }
211
212 #[test]
213 fn evaluates_boolean_not_or() {
214 let e = parse("NOT (x = 0 OR y = 0)").unwrap();
215 let r = row(&[("x", Value::Int(1)), ("y", Value::Int(2))]);
216 assert_eq!(e.evaluate(&r, &[]), Ok(true));
217 }
218
219 #[test]
220 fn evaluates_param() {
221 let e = parse("color = %0").unwrap();
222 let r = row(&[("color", Value::String("BLUE".into()))]);
223 assert_eq!(e.evaluate(&r, &[Value::String("BLUE".into())]), Ok(true),);
224 }
225
226 #[test]
227 fn missing_param_is_error() {
228 let e = parse("color = %0").unwrap();
229 let r = row(&[("color", Value::String("BLUE".into()))]);
230 assert_eq!(e.evaluate(&r, &[]), Err(EvalError::MissingParam(0)),);
231 }
232
233 #[test]
234 fn unknown_field_is_error() {
235 let e = parse("missing = 1").unwrap();
236 let r = row(&[("x", Value::Int(1))]);
237 assert!(matches!(
238 e.evaluate(&r, &[]),
239 Err(EvalError::UnknownField(_))
240 ));
241 }
242
243 #[test]
244 fn like_wildcards() {
245 let e = parse("name LIKE 'foo%'").unwrap();
246 let r_yes = row(&[("name", Value::String("foobar".into()))]);
247 let r_no = row(&[("name", Value::String("barfoo".into()))]);
248 assert_eq!(e.evaluate(&r_yes, &[]), Ok(true));
249 assert_eq!(e.evaluate(&r_no, &[]), Ok(false));
250
251 let single = parse("name LIKE 'a_c'").unwrap();
252 let r_yes = row(&[("name", Value::String("abc".into()))]);
253 let r_no = row(&[("name", Value::String("abbc".into()))]);
254 assert_eq!(single.evaluate(&r_yes, &[]), Ok(true));
255 assert_eq!(single.evaluate(&r_no, &[]), Ok(false));
256 }
257
258 #[test]
259 fn like_on_non_string_rejected() {
260 let e = parse("x LIKE 5").unwrap();
261 let r = row(&[("x", Value::Int(5))]);
262 assert!(matches!(
263 e.evaluate(&r, &[]),
264 Err(EvalError::TypeMismatch(_))
265 ));
266 }
267}