Skip to main content

ravenrs_criteria/
eval.rs

1//! In-memory evaluation over any kind of row. This interprets the parsed tree
2//! of the criteria language; it never runs host code, except the functions a
3//! program registers on purpose.
4
5use std::cell::{Cell, RefCell};
6use std::collections::{HashMap, HashSet};
7use std::rc::Rc;
8
9use crate::ast::{AggregateCall, BinaryOp, Criteria, GroupOp, UnaryOp};
10use crate::format::{Culture, format_composite};
11use crate::functions::{Aggregate, Function};
12use crate::value::{DateTime, Value};
13
14/// A row expressions can read: a JSON object, a grid row, an entity.
15pub trait Record: Sized {
16    /// The value at a field path; `None` when there is no such field.
17    fn field(&self, path: &[String]) -> Option<Value>;
18
19    /// The rows of a nested collection; `None` when there is no such collection.
20    fn collection(&self, path: &[String]) -> Option<Vec<&Self>>;
21}
22
23#[cfg(feature = "json")]
24fn json_at<'j>(base: &'j serde_json::Value, path: &[String]) -> Option<&'j serde_json::Value> {
25    let mut cur = base;
26    for seg in path {
27        cur = match cur {
28            serde_json::Value::Object(map) => map.get(seg)?,
29            serde_json::Value::Array(items) => items.get(seg.parse::<usize>().ok()?)?,
30            _ => return None,
31        };
32    }
33    Some(cur)
34}
35
36#[cfg(feature = "json")]
37impl Record for serde_json::Value {
38    fn field(&self, path: &[String]) -> Option<Value> {
39        json_at(self, path).map(Value::from_json)
40    }
41
42    fn collection(&self, path: &[String]) -> Option<Vec<&Self>> {
43        match json_at(self, path)? {
44            serde_json::Value::Array(items) => Some(items.iter().collect()),
45            _ => None,
46        }
47    }
48}
49
50impl Record for HashMap<String, Value> {
51    fn field(&self, path: &[String]) -> Option<Value> {
52        self.get(&path.join(".")).cloned()
53    }
54
55    fn collection(&self, _path: &[String]) -> Option<Vec<&Self>> {
56        None
57    }
58}
59
60/// A function a program adds to the language.
61pub type CustomFunction = Rc<dyn Fn(&[Value]) -> Result<Value, String>>;
62
63/// What every evaluation shares: parameters, calculated fields, custom
64/// functions, the culture and the clock. Keep one per run; it is not thread-safe.
65pub struct Environment {
66    parameters: HashMap<String, Value>,
67    calculated: HashMap<String, Criteria>,
68    functions: HashMap<String, CustomFunction>,
69    culture: Culture,
70    now: DateTime,
71    /// Running sums by (expression, scope): the last row index and the total so far.
72    running: RefCell<HashMap<(usize, usize), (usize, f64)>>,
73    depth: Cell<u32>,
74    random: Cell<u64>,
75    error: RefCell<Option<EvalError>>,
76}
77
78impl std::fmt::Debug for Environment {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        f.debug_struct("Environment").field("culture", &self.culture.name).finish_non_exhaustive()
81    }
82}
83
84impl Default for Environment {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl Environment {
91    /// An environment in US English, on the system clock.
92    pub fn new() -> Self {
93        let now = DateTime::now();
94        Self {
95            parameters: HashMap::new(),
96            calculated: HashMap::new(),
97            functions: HashMap::new(),
98            culture: Culture::en_us(),
99            now,
100            running: RefCell::new(HashMap::new()),
101            depth: Cell::new(0),
102            random: Cell::new(
103                (now.days as u64).wrapping_mul(86_400) ^ u64::from(now.seconds) ^ 0x9e37_79b9_7f4a_7c15,
104            ),
105            error: RefCell::new(None),
106        }
107    }
108
109    /// Sets the culture for formats.
110    pub fn culture(mut self, culture: Culture) -> Self {
111        self.culture = culture;
112        self
113    }
114
115    /// The culture in effect, for formatting outside expressions.
116    pub fn formats(&self) -> &Culture {
117        &self.culture
118    }
119
120    /// Fixes the clock (`Today()`, `Now()`), for tests and reproducible runs.
121    pub fn at(mut self, now: DateTime) -> Self {
122        self.now = now;
123        self
124    }
125
126    /// Sets a parameter, read as `?name`.
127    pub fn parameter(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
128        self.parameters.insert(name.into(), value.into());
129        self
130    }
131
132    /// Adds a calculated field, read as `[name]`.
133    pub fn calculated(mut self, name: impl Into<String>, expression: Criteria) -> Self {
134        self.calculated.insert(name.into(), expression);
135        self
136    }
137
138    /// Adds a function to the language; names are case-insensitive.
139    ///
140    /// ```
141    /// use ravenrs_criteria::{Criteria, Environment, Scope, Value};
142    ///
143    /// let env = Environment::new().register("Double", |args| {
144    ///     Ok(Value::Number(args[0].as_number().unwrap_or(0.0) * 2.0))
145    /// });
146    /// let c = Criteria::parse("double(21)").expect("valid");
147    /// let row = serde_json::json!({});
148    /// assert_eq!(Scope::new(&env).row(&row).evaluate(&c), Ok(Value::Number(42.0)));
149    /// ```
150    pub fn register(
151        mut self,
152        name: &str,
153        function: impl Fn(&[Value]) -> Result<Value, String> + 'static,
154    ) -> Self {
155        self.functions.insert(name.to_lowercase(), Rc::new(function));
156        self
157    }
158
159    /// Whether a custom function with this name is registered.
160    pub fn has_function(&self, name: &str) -> bool {
161        self.functions.contains_key(&name.to_lowercase())
162    }
163
164    fn fail(&self, error: EvalError) {
165        self.error.borrow_mut().get_or_insert(error);
166    }
167}
168
169/// A problem while evaluating. Type mismatches are not errors: they give
170/// `Null`, as a missing value would, so a report still prints.
171#[derive(Clone, Debug, PartialEq, Eq)]
172#[non_exhaustive]
173pub enum EvalError {
174    /// A function that is neither built in nor registered.
175    UnknownFunction(String),
176    /// A registered function reported a failure.
177    FunctionFailed {
178        /// The function.
179        name: String,
180        /// What it reported.
181        message: String,
182    },
183    /// Calculated fields that depend on each other in a cycle.
184    Cycle(String),
185}
186
187impl std::fmt::Display for EvalError {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        match self {
190            Self::UnknownFunction(name) => write!(f, "unknown function '{name}'"),
191            Self::FunctionFailed { name, message } => write!(f, "function '{name}' failed: {message}"),
192            Self::Cycle(name) => write!(f, "calculated field '{name}' depends on itself"),
193        }
194    }
195}
196
197impl std::error::Error for EvalError {}
198
199/// Where an expression is evaluated: the row, its parent, the rows aggregates
200/// run over, and the page.
201pub struct Scope<'a, R> {
202    env: &'a Environment,
203    row: Option<&'a R>,
204    parent: Option<&'a R>,
205    rows: &'a [&'a R],
206    position: Option<usize>,
207    page: Option<(usize, usize)>,
208}
209
210impl<R> std::fmt::Debug for Scope<'_, R> {
211    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212        f.debug_struct("Scope")
213            .field("rows", &self.rows.len())
214            .field("position", &self.position)
215            .finish_non_exhaustive()
216    }
217}
218
219impl<R> Clone for Scope<'_, R> {
220    fn clone(&self) -> Self {
221        *self
222    }
223}
224
225impl<R> Copy for Scope<'_, R> {}
226
227impl<'a, R: Record> Scope<'a, R> {
228    /// A scope with no row.
229    pub fn new(env: &'a Environment) -> Self {
230        Self { env, row: None, parent: None, rows: &[], position: None, page: None }
231    }
232
233    /// The current row.
234    pub fn row(mut self, row: &'a R) -> Self {
235        self.row = Some(row);
236        self
237    }
238
239    /// Sets or clears the current row.
240    pub fn maybe_row(mut self, row: Option<&'a R>) -> Self {
241        self.row = row;
242        self
243    }
244
245    /// The parent row, read as `[^.Field]`.
246    pub fn parent(mut self, parent: Option<&'a R>) -> Self {
247        self.parent = parent;
248        self
249    }
250
251    /// The rows aggregates run over (a group, a page, a whole list).
252    pub fn rows(mut self, rows: &'a [&'a R]) -> Self {
253        self.rows = rows;
254        self
255    }
256
257    /// The current row's index in [`Self::rows`], for running aggregates.
258    pub fn position(mut self, position: Option<usize>) -> Self {
259        self.position = position;
260        self
261    }
262
263    /// The page number and page count, for `PageNumber()` and `PageCount()`.
264    pub fn page(mut self, number: usize, count: usize) -> Self {
265        self.page = Some((number, count));
266        self
267    }
268
269    /// The current row, if there is one.
270    pub fn current_row(&self) -> Option<&'a R> {
271        self.row
272    }
273
274    /// The rows aggregates run over.
275    pub fn current_rows(&self) -> &'a [&'a R] {
276        self.rows
277    }
278
279    /// Evaluates `criteria`.
280    pub fn evaluate(&self, criteria: &Criteria) -> Result<Value, EvalError> {
281        let value = self.node(criteria);
282        match self.env.error.borrow_mut().take() {
283            Some(e) => Err(e),
284            None => Ok(value),
285        }
286    }
287
288    /// Evaluates a filter: true when it holds; errors count as false.
289    pub fn matches(&self, criteria: &Criteria) -> bool {
290        self.evaluate(criteria).is_ok_and(|v| v.is_true())
291    }
292
293    fn field(&self, path: &[String]) -> Value {
294        let (base, path) = match path.split_first() {
295            Some((first, rest)) if first == "^" => (self.parent, rest),
296            _ => (self.row, path),
297        };
298        if path.len() == 1
299            && let Some(calc) = self.env.calculated.get(&path[0])
300        {
301            if self.env.depth.get() > 32 {
302                self.env.fail(EvalError::Cycle(path[0].clone()));
303                return Value::Null;
304            }
305            self.env.depth.set(self.env.depth.get() + 1);
306            let v = self.node(calc);
307            self.env.depth.set(self.env.depth.get() - 1);
308            return v;
309        }
310        base.and_then(|b| b.field(path)).unwrap_or_default()
311    }
312
313    fn node(&self, n: &Criteria) -> Value {
314        match n {
315            Criteria::Value(v) => v.clone(),
316            Criteria::Property(path) => self.field(path),
317            Criteria::Parameter(name) => self.env.parameters.get(name).cloned().unwrap_or_default(),
318            Criteria::Unary(op, a) => {
319                let v = self.node(a);
320                match op {
321                    UnaryOp::Not => Value::Bool(!v.is_true()),
322                    UnaryOp::Minus => v.as_number().map_or(Value::Null, |x| Value::Number(-x)),
323                    UnaryOp::BitNot => {
324                        v.as_number().map_or(Value::Null, |x| Value::Number(!(x as i64) as f64))
325                    }
326                }
327            }
328            Criteria::Group(GroupOp::And, items) => Value::Bool(items.iter().all(|c| self.node(c).is_true())),
329            Criteria::Group(GroupOp::Or, items) => Value::Bool(items.iter().any(|c| self.node(c).is_true())),
330            Criteria::Binary(op, a, b) => binary(*op, self.node(a), self.node(b)),
331            Criteria::In(a, items) => {
332                let v = self.node(a);
333                Value::Bool(items.iter().any(|i| v.loose_eq(&self.node(i))))
334            }
335            Criteria::Between(a, lo, hi) => {
336                let v = self.node(a);
337                if v.is_null() {
338                    return Value::Bool(false);
339                }
340                Value::Bool(v.compare(&self.node(lo)).is_ge() && v.compare(&self.node(hi)).is_le())
341            }
342            Criteria::IsNull(a) => Value::Bool(self.node(a).is_null()),
343            Criteria::Function(f, args) => self.call(*f, args),
344            Criteria::Custom(name, args) => {
345                let Some(function) = self.env.functions.get(&name.to_lowercase()).cloned() else {
346                    self.env.fail(EvalError::UnknownFunction(name.clone()));
347                    return Value::Null;
348                };
349                let values: Vec<Value> = args.iter().map(|a| self.node(a)).collect();
350                function(&values).unwrap_or_else(|message| {
351                    self.env.fail(EvalError::FunctionFailed { name: name.clone(), message });
352                    Value::Null
353                })
354            }
355            Criteria::Aggregate(call) => self.aggregate_call(call),
356        }
357    }
358
359    fn aggregate_call(&self, call: &AggregateCall) -> Value {
360        let key = std::ptr::from_ref(call) as usize;
361        match &call.collection {
362            None => {
363                let filtered: Vec<&R>;
364                let rows: &[&R] = match &call.condition {
365                    Some(cond) => {
366                        filtered = self
367                            .rows
368                            .iter()
369                            .copied()
370                            .filter(|r| self.at_row(r, None).node(cond).is_true())
371                            .collect();
372                        &filtered
373                    }
374                    None => self.rows,
375                };
376                self.aggregate(call, rows, key)
377            }
378            Some(path) => {
379                let base = match path.split_first() {
380                    Some((first, rest)) if first == "^" => self.parent.and_then(|p| p.collection(rest)),
381                    _ => self.row.and_then(|r| r.collection(path)),
382                };
383                let Some(items) = base else {
384                    return match call.function {
385                        Aggregate::Count | Aggregate::DistinctCount => Value::Number(0.0),
386                        Aggregate::Exists => Value::Bool(false),
387                        _ => Value::Null,
388                    };
389                };
390                // Inside the collection, `^` reads the row that owns it.
391                let inner = Scope { parent: self.row, position: None, ..*self };
392                let rows: Vec<&R> = match &call.condition {
393                    Some(cond) => {
394                        items.into_iter().filter(|r| inner.at_row(r, None).node(cond).is_true()).collect()
395                    }
396                    None => items,
397                };
398                // The rows live only for this call, so they stay out of the scope's lifetime.
399                aggregate_over(self.env, call, &rows, self.row, None, self.page, key)
400            }
401        }
402    }
403
404    fn at_row(&self, row: &'a R, position: Option<usize>) -> Self {
405        Scope { row: Some(row), position, ..*self }
406    }
407
408    fn aggregate(&self, call: &AggregateCall, rows: &[&R], key: usize) -> Value {
409        aggregate_over(
410            self.env,
411            call,
412            rows,
413            self.parent,
414            self.position.filter(|_| rows.len() == self.rows.len()),
415            self.page,
416            key,
417        )
418    }
419
420    #[allow(clippy::too_many_lines, reason = "one arm per function is the clearest form")]
421    fn call(&self, f: Function, args: &[Criteria]) -> Value {
422        use Function as F;
423        let a = |i: usize| args.get(i).map(|n| self.node(n)).unwrap_or_default();
424        let text = |i: usize| a(i).to_text();
425        let num = |i: usize| a(i).as_number();
426        let date = |i: usize| a(i).as_date();
427        let math = |g: fn(f64) -> f64| num(0).map_or(Value::Null, |x| Value::Number(g(x)));
428        let part = |g: fn(DateTime) -> i64| date(0).map_or(Value::Null, |d| Value::Number(g(d) as f64));
429        let add = |unit: i64| match (date(0), num(1)) {
430            (Some(d), Some(n)) => Value::Date(add_seconds(d, n as i64 * unit)),
431            _ => Value::Null,
432        };
433        let diff = |g: fn(DateTime, DateTime) -> i64| match (date(0), date(1)) {
434            (Some(x), Some(y)) => Value::Number(g(x, y) as f64),
435            _ => Value::Null,
436        };
437        let now = self.env.now;
438        let today = DateTime { seconds: 0, ..now };
439        let month_start = |d: DateTime| {
440            let (y, m, _) = d.ymd();
441            DateTime::from_ymd(y, m, 1).unwrap_or(d)
442        };
443        let year_start = |d: DateTime| DateTime::from_ymd(d.ymd().0, 1, 1).unwrap_or(d);
444        let week_start = |d: DateTime| DateTime { seconds: 0, ..d.add_days(-i64::from(d.weekday())) };
445        let month_is = |m: u32| Value::Bool(date(0).is_some_and(|d| d.ymd().1 == m));
446        let in_days = |from: DateTime, to: DateTime| {
447            Value::Bool(date(0).is_some_and(|d| d.days >= from.days && d.days < to.days))
448        };
449        match f {
450            F::Iif => {
451                let mut i = 0;
452                while i + 1 < args.len() {
453                    if a(i).is_true() {
454                        return a(i + 1);
455                    }
456                    i += 2;
457                }
458                a(args.len() - 1)
459            }
460            F::IsNull if args.len() == 1 => Value::Bool(a(0).is_null()),
461            F::IsNull => match a(0) {
462                Value::Null => a(1),
463                v => v,
464            },
465            F::IsNullOrEmpty => Value::Bool(match a(0) {
466                Value::Null => true,
467                Value::Text(s) => s.is_empty(),
468                _ => false,
469            }),
470            F::Ascii => text(0).chars().next().map_or(Value::Null, |c| Value::Number(f64::from(c as u32))),
471            F::Char => num(0)
472                .and_then(|n| char::from_u32(n as u32))
473                .map_or(Value::Null, |c| Value::Text(c.to_string())),
474            F::CharIndex => {
475                let (needle, hay) = (text(0), text(1));
476                let start = num(2).unwrap_or(0.0).max(0.0) as usize;
477                let skip: usize = hay.chars().take(start).map(char::len_utf8).sum();
478                let found = hay[skip..].find(&needle).map(|b| start + hay[skip..skip + b].chars().count());
479                Value::Number(found.map_or(-1.0, |i| i as f64))
480            }
481            F::Concat => Value::Text((0..args.len()).map(text).collect()),
482            F::Contains => Value::Bool(text(0).contains(&text(1))),
483            F::EndsWith => Value::Bool(text(0).ends_with(&text(1))),
484            F::Insert => {
485                let mut chars: Vec<char> = text(0).chars().collect();
486                let at = (num(1).unwrap_or(0.0).max(0.0) as usize).min(chars.len());
487                chars.splice(at..at, text(2).chars());
488                Value::Text(chars.into_iter().collect())
489            }
490            F::Len => Value::Number(text(0).chars().count() as f64),
491            F::Lower => Value::Text(text(0).to_lowercase()),
492            F::NewLine => Value::Text("\n".into()),
493            F::PadLeft | F::PadRight => {
494                let s = text(0);
495                let width = num(1).unwrap_or(0.0).max(0.0) as usize;
496                let fill = text(2).chars().next().unwrap_or(' ');
497                let pad: String =
498                    std::iter::repeat_n(fill, width.saturating_sub(s.chars().count())).collect();
499                Value::Text(if f == F::PadLeft { pad + &s } else { s + &pad })
500            }
501            F::Remove => {
502                let chars: Vec<char> = text(0).chars().collect();
503                let start = (num(1).unwrap_or(0.0).max(0.0) as usize).min(chars.len());
504                let count = num(2).map_or(chars.len() - start, |n| n.max(0.0) as usize);
505                let end = start.saturating_add(count).min(chars.len());
506                Value::Text(chars[..start].iter().chain(&chars[end..]).collect())
507            }
508            F::Replace => Value::Text(text(0).replace(&text(1), &text(2))),
509            F::Reverse => Value::Text(text(0).chars().rev().collect()),
510            F::StartsWith => Value::Bool(text(0).starts_with(&text(1))),
511            F::Substring => {
512                let start = num(1).unwrap_or(0.0).max(0.0) as usize;
513                let count = num(2).map_or(usize::MAX, |n| n.max(0.0) as usize);
514                Value::Text(text(0).chars().skip(start).take(count).collect())
515            }
516            F::ToStr => Value::Text(text(0)),
517            F::Trim => Value::Text(text(0).trim().to_owned()),
518            F::Upper => Value::Text(text(0).to_uppercase()),
519            F::Abs => math(f64::abs),
520            F::Acos => math(f64::acos),
521            F::Asin => math(f64::asin),
522            F::Atn => math(f64::atan),
523            F::Atn2 => match (num(0), num(1)) {
524                (Some(y), Some(x)) => Value::Number(y.atan2(x)),
525                _ => Value::Null,
526            },
527            F::BigMul => match (num(0), num(1)) {
528                (Some(x), Some(y)) => Value::Number(x * y),
529                _ => Value::Null,
530            },
531            F::Ceiling => math(f64::ceil),
532            F::Cos => math(f64::cos),
533            F::Cosh => math(f64::cosh),
534            F::Exp => math(f64::exp),
535            F::Floor => math(f64::floor),
536            F::Log => match (num(0).filter(|x| *x > 0.0), num(1)) {
537                (Some(x), Some(base)) => Value::Number(x.log(base)),
538                (Some(x), None) => Value::Number(x.ln()),
539                _ => Value::Null,
540            },
541            F::Log10 => num(0).filter(|x| *x > 0.0).map_or(Value::Null, |x| Value::Number(x.log10())),
542            F::Max2 | F::Min2 => {
543                let (x, y) = (a(0), a(1));
544                let first = if f == F::Min2 { x.compare(&y).is_le() } else { x.compare(&y).is_ge() };
545                if first { x } else { y }
546            }
547            F::Power => match (num(0), num(1)) {
548                (Some(x), Some(y)) => Value::Number(x.powf(y)),
549                _ => Value::Null,
550            },
551            F::Rnd => {
552                let mut x = self.env.random.get();
553                x ^= x << 13;
554                x ^= x >> 7;
555                x ^= x << 17;
556                self.env.random.set(x);
557                Value::Number((x >> 11) as f64 / (1u64 << 53) as f64)
558            }
559            // Half away from zero, as accountants expect.
560            F::Round => num(0).map_or(Value::Null, |x| {
561                let p = 10f64.powi(num(1).unwrap_or(0.0) as i32);
562                Value::Number((x * p).round() / p)
563            }),
564            F::Sign => num(0).map_or(Value::Null, |x| Value::Number(if x == 0.0 { 0.0 } else { x.signum() })),
565            F::Sin => math(f64::sin),
566            F::Sinh => math(f64::sinh),
567            F::Sqr | F::Sqrt => num(0).filter(|x| *x >= 0.0).map_or(Value::Null, |x| Value::Number(x.sqrt())),
568            F::Tan => math(f64::tan),
569            F::Tanh => math(f64::tanh),
570            F::ToDecimal | F::ToDouble | F::ToFloat | F::ToNumber => {
571                num(0).map_or(Value::Null, Value::Number)
572            }
573            F::ToInt | F::ToLong => math(f64::trunc),
574            F::AddDays => add(86_400),
575            F::AddHours => add(3600),
576            F::AddMinutes => add(60),
577            F::AddSeconds => add(1),
578            F::AddMonths | F::AddYears => match (date(0), num(1)) {
579                (Some(d), Some(n)) => {
580                    Value::Date(d.add_months(n as i64 * if f == F::AddYears { 12 } else { 1 }))
581                }
582                _ => Value::Null,
583            },
584            F::DateDiffDay => diff(|x, y| y.days - x.days),
585            F::DateDiffHour => diff(|x, y| (total_seconds(y) - total_seconds(x)) / 3600),
586            F::DateDiffMinute => diff(|x, y| (total_seconds(y) - total_seconds(x)) / 60),
587            F::DateDiffSecond => diff(|x, y| total_seconds(y) - total_seconds(x)),
588            F::DateDiffMonth => diff(|x, y| {
589                let (xy, xm, _) = x.ymd();
590                let (yy, ym, _) = y.ymd();
591                (i64::from(yy) - i64::from(xy)) * 12 + i64::from(ym) - i64::from(xm)
592            }),
593            F::DateDiffYear => diff(|x, y| i64::from(y.ymd().0) - i64::from(x.ymd().0)),
594            F::DateTimeFromParts => {
595                let n = |i: usize, default: f64| num(i).unwrap_or(default);
596                let d = DateTime::from_ymd(n(0, 0.0) as i32, n(1, 0.0) as u32, n(2, 0.0) as u32);
597                d.map_or(Value::Null, |d| {
598                    Value::Date(DateTime {
599                        seconds: (n(3, 0.0) * 3600.0 + n(4, 0.0) * 60.0 + n(5, 0.0)) as u32,
600                        ..d
601                    })
602                })
603            }
604            F::GetDate => date(0).map_or(Value::Null, |d| Value::Date(DateTime { seconds: 0, ..d })),
605            F::GetDay => part(|d| i64::from(d.ymd().2)),
606            F::GetDayOfWeek => part(|d| i64::from(d.weekday())),
607            F::GetDayOfYear => {
608                part(|d| d.days - DateTime::from_ymd(d.ymd().0, 1, 1).map_or(d.days, |s| s.days) + 1)
609            }
610            F::GetHour => part(|d| i64::from(d.hms().0)),
611            F::GetMinute => part(|d| i64::from(d.hms().1)),
612            F::GetMonth => part(|d| i64::from(d.ymd().1)),
613            F::GetSecond => part(|d| i64::from(d.hms().2)),
614            F::GetYear => part(|d| i64::from(d.ymd().0)),
615            F::Now | F::UtcNow | F::LocalDateTimeNow => Value::Date(now),
616            F::Today | F::LocalDateTimeToday => Value::Date(today),
617            F::LocalDateTimeTomorrow => Value::Date(today.add_days(1)),
618            F::LocalDateTimeYesterday => Value::Date(today.add_days(-1)),
619            F::LocalDateTimeDayAfterTomorrow => Value::Date(today.add_days(2)),
620            F::LocalDateTimeThisWeek => Value::Date(week_start(now)),
621            F::LocalDateTimeLastWeek => Value::Date(week_start(now).add_days(-7)),
622            F::LocalDateTimeNextWeek => Value::Date(week_start(now).add_days(7)),
623            F::LocalDateTimeTwoWeeksAway => Value::Date(week_start(now).add_days(14)),
624            F::LocalDateTimeThisMonth => Value::Date(month_start(now)),
625            F::LocalDateTimeLastMonth => Value::Date(month_start(now).add_months(-1)),
626            F::LocalDateTimeNextMonth => Value::Date(month_start(now).add_months(1)),
627            F::LocalDateTimeTwoMonthsAway => Value::Date(month_start(now).add_months(2)),
628            F::LocalDateTimeThisYear => Value::Date(year_start(now)),
629            F::LocalDateTimeLastYear => Value::Date(year_start(now).add_months(-12)),
630            F::LocalDateTimeNextYear => Value::Date(year_start(now).add_months(12)),
631            F::LocalDateTimeTwoYearsAway => Value::Date(year_start(now).add_months(24)),
632            F::LocalDateTimeYearBeforeToday => Value::Date(today.add_months(-12)),
633            F::InDateRange => match (date(0), date(1), date(2)) {
634                (Some(d), Some(lo), Some(hi)) => Value::Bool(d.days >= lo.days && d.days <= hi.days),
635                _ => Value::Bool(false),
636            },
637            F::InTimeRange | F::OutOfTimeRange => match (date(0), date(1), date(2)) {
638                (Some(d), Some(lo), Some(hi)) => {
639                    let (t, lo, hi) = (d.seconds, lo.seconds, hi.seconds);
640                    // A range whose end comes before its start spans midnight.
641                    let inside = if lo <= hi { t >= lo && t <= hi } else { t >= lo || t <= hi };
642                    Value::Bool(inside == (f == F::InTimeRange))
643                }
644                _ => Value::Bool(false),
645            },
646            F::IsSameDay => match date(0) {
647                Some(d) => Value::Bool((1..args.len()).any(|i| date(i).is_some_and(|o| o.days == d.days))),
648                None => Value::Bool(false),
649            },
650            F::IsThisWeek => in_days(week_start(now), week_start(now).add_days(7)),
651            F::IsThisMonth => in_days(month_start(now), month_start(now).add_months(1)),
652            F::IsLastMonth => in_days(month_start(now).add_months(-1), month_start(now)),
653            F::IsNextMonth => in_days(month_start(now).add_months(1), month_start(now).add_months(2)),
654            F::IsThisYear => in_days(year_start(now), year_start(now).add_months(12)),
655            F::IsLastYear => in_days(year_start(now).add_months(-12), year_start(now)),
656            F::IsNextYear => in_days(year_start(now).add_months(12), year_start(now).add_months(24)),
657            F::IsYearToDate => in_days(year_start(now), today.add_days(1)),
658            F::IsJanuary => month_is(1),
659            F::IsFebruary => month_is(2),
660            F::IsMarch => month_is(3),
661            F::IsApril => month_is(4),
662            F::IsMay => month_is(5),
663            F::IsJune => month_is(6),
664            F::IsJuly => month_is(7),
665            F::IsAugust => month_is(8),
666            F::IsSeptember => month_is(9),
667            F::IsOctober => month_is(10),
668            F::IsNovember => month_is(11),
669            F::IsDecember => month_is(12),
670            F::FormatString => {
671                let rest: Vec<Value> = (1..args.len()).map(a).collect();
672                Value::Text(format_composite(&text(0), &rest, &self.env.culture))
673            }
674            F::Rgb => color(&[num(0), num(1), num(2)]),
675            F::Argb => color(&[num(1), num(2), num(3), num(0)]),
676            F::PageNumber => self.page.map_or(Value::Null, |(n, _)| Value::Number(n as f64)),
677            F::PageCount => self.page.map_or(Value::Null, |(_, c)| Value::Number(c as f64)),
678            F::CurrentRowIndexInGroup => self.position.map_or(Value::Null, |p| Value::Number(p as f64)),
679            F::PrevRowColumnValue | F::NextRowColumnValue => {
680                let Some(p) = self.position else { return Value::Null };
681                let at = if f == F::PrevRowColumnValue { p.checked_sub(1) } else { p.checked_add(1) };
682                match at.and_then(|i| self.rows.get(i)) {
683                    Some(row) => self.at_row(row, at).node(&args[0]),
684                    None => Value::Null,
685                }
686            }
687        }
688    }
689}
690
691fn total_seconds(d: DateTime) -> i64 {
692    d.days * 86_400 + i64::from(d.seconds)
693}
694
695fn add_seconds(d: DateTime, n: i64) -> DateTime {
696    let t = total_seconds(d) + n;
697    DateTime { days: t.div_euclid(86_400), seconds: t.rem_euclid(86_400) as u32 }
698}
699
700fn color(parts: &[Option<f64>]) -> Value {
701    let mut s = String::from("#");
702    for p in parts {
703        let Some(p) = p else { return Value::Null };
704        s.push_str(&format!("{:02x}", p.clamp(0.0, 255.0) as u8));
705    }
706    Value::Text(s)
707}
708
709fn variance(values: &[f64], population: bool) -> Option<f64> {
710    let n = values.len() as f64;
711    let denominator = if population { n } else { n - 1.0 };
712    if values.is_empty() || denominator <= 0.0 {
713        return None;
714    }
715    let mean = values.iter().sum::<f64>() / n;
716    Some(values.iter().map(|v| (v - mean) * (v - mean)).sum::<f64>() / denominator)
717}
718
719fn number(v: Option<f64>) -> Value {
720    v.map_or(Value::Null, Value::Number)
721}
722
723/// Runs an aggregate over `rows`. `parent` is what `^` reads inside.
724#[allow(clippy::too_many_lines, reason = "one arm per aggregate is the clearest form")]
725fn aggregate_over<R: Record>(
726    env: &Environment,
727    call: &AggregateCall,
728    rows: &[&R],
729    parent: Option<&R>,
730    position: Option<usize>,
731    page: Option<(usize, usize)>,
732    key: usize,
733) -> Value {
734    let scope_at = |i: usize| Scope { env, row: Some(rows[i]), parent, rows, position: Some(i), page };
735    let arg = |i: usize, n: usize| -> Value {
736        match call.args.get(n) {
737            Some(a) => scope_at(i).node(a),
738            None => Value::Bool(true),
739        }
740    };
741    let values = || -> Vec<Value> { (0..rows.len()).map(|i| arg(i, 0)).collect() };
742    let numbers = || -> Vec<f64> { values().iter().filter_map(Value::as_number).collect() };
743    let distinct = || -> Vec<f64> {
744        let mut seen = HashSet::new();
745        numbers().into_iter().filter(|n| seen.insert(n.to_bits())).collect()
746    };
747    use Aggregate as A;
748    match call.function {
749        A::Count if call.args.is_empty() => Value::Number(rows.len() as f64),
750        A::Count => Value::Number(values().iter().filter(|v| !v.is_null()).count() as f64),
751        A::Sum => Value::Number(numbers().iter().sum()),
752        A::Avg => {
753            let n = numbers();
754            if n.is_empty() { Value::Null } else { Value::Number(n.iter().sum::<f64>() / n.len() as f64) }
755        }
756        A::Min | A::Max => values()
757            .into_iter()
758            .filter(|v| !v.is_null())
759            .reduce(|a, b| {
760                let take_b =
761                    if call.function == A::Min { b.compare(&a).is_lt() } else { b.compare(&a).is_gt() };
762                if take_b { b } else { a }
763            })
764            .unwrap_or_default(),
765        A::Exists if call.args.is_empty() => Value::Bool(!rows.is_empty()),
766        A::Exists => Value::Bool((0..rows.len()).any(|i| arg(i, 0).is_true())),
767        A::Single => match rows.len() {
768            1 if call.args.is_empty() => Value::Bool(true),
769            1 => arg(0, 0),
770            _ => Value::Null,
771        },
772        A::Join => {
773            let sep = call.args.get(1).map_or_else(
774                || ", ".to_owned(),
775                |c| Scope { env, row: None, parent, rows, position: None, page }.node(c).to_text(),
776            );
777            Value::Text(
778                values().iter().filter(|v| !v.is_null()).map(Value::to_text).collect::<Vec<_>>().join(&sep),
779            )
780        }
781        A::First => {
782            if rows.is_empty() {
783                Value::Null
784            } else {
785                arg(0, 0)
786            }
787        }
788        A::Last => {
789            if rows.is_empty() {
790                Value::Null
791            } else {
792                arg(rows.len() - 1, 0)
793            }
794        }
795        A::Median => {
796            let mut n = numbers();
797            if n.is_empty() {
798                return Value::Null;
799            }
800            n.sort_by(f64::total_cmp);
801            let mid = n.len() / 2;
802            Value::Number(if n.len() % 2 == 1 { n[mid] } else { (n[mid - 1] + n[mid]) / 2.0 })
803        }
804        A::Var => number(variance(&numbers(), false)),
805        A::VarP => number(variance(&numbers(), true)),
806        A::StdDev => number(variance(&numbers(), false).map(f64::sqrt)),
807        A::StdDevP => number(variance(&numbers(), true).map(f64::sqrt)),
808        A::DistinctSum => Value::Number(distinct().iter().sum()),
809        A::DistinctCount => {
810            let set: HashSet<String> =
811                values().iter().filter(|v| !v.is_null()).map(|v| format!("{v:?}")).collect();
812            Value::Number(set.len() as f64)
813        }
814        A::DistinctAvg => {
815            let n = distinct();
816            if n.is_empty() { Value::Null } else { Value::Number(n.iter().sum::<f64>() / n.len() as f64) }
817        }
818        A::DistinctVar => number(variance(&distinct(), false)),
819        A::DistinctVarP => number(variance(&distinct(), true)),
820        A::DistinctStdDev => number(variance(&distinct(), false).map(f64::sqrt)),
821        A::DistinctStdDevP => number(variance(&distinct(), true).map(f64::sqrt)),
822        A::WeightedAvg => {
823            let (mut total, mut weight) = (0.0, 0.0);
824            for i in 0..rows.len() {
825                if let (Some(v), Some(w)) = (arg(i, 0).as_number(), arg(i, 1).as_number()) {
826                    total += v * w;
827                    weight += w;
828                }
829            }
830            if weight == 0.0 { Value::Null } else { Value::Number(total / weight) }
831        }
832        A::RecordNumber => position.map_or(Value::Null, |p| Value::Number(p as f64 + 1.0)),
833        A::Percentage => {
834            let total: f64 = numbers().iter().sum();
835            match position.filter(|p| *p < rows.len()) {
836                Some(p) if total != 0.0 => number(arg(p, 0).as_number().map(|v| v / total)),
837                _ => Value::Null,
838            }
839        }
840        A::RunningSum => {
841            // Up to and including the current row; incremental across rows.
842            let Some(upto) = position.filter(|p| *p < rows.len()) else {
843                return Value::Number(numbers().iter().sum());
844            };
845            let cache_key = (key, rows.as_ptr() as usize);
846            let cached = env.running.borrow().get(&cache_key).copied();
847            let total = match cached {
848                Some((last, acc)) if last + 1 == upto => acc + arg(upto, 0).as_number().unwrap_or(0.0),
849                Some((last, acc)) if last == upto => acc,
850                _ => (0..=upto).filter_map(|i| arg(i, 0).as_number()).sum(),
851            };
852            env.running.borrow_mut().insert(cache_key, (upto, total));
853            Value::Number(total)
854        }
855    }
856}
857
858/// `%` matches any run of characters, `_` any one character; case-sensitive.
859fn like(text: &str, pattern: &str) -> bool {
860    let (t, p): (Vec<char>, Vec<char>) = (text.chars().collect(), pattern.chars().collect());
861    let (mut ti, mut pi, mut star, mut mark) = (0, 0, None, 0);
862    while ti < t.len() {
863        if pi < p.len() && (p[pi] == '_' || (p[pi] != '%' && p[pi] == t[ti])) {
864            ti += 1;
865            pi += 1;
866        } else if pi < p.len() && p[pi] == '%' {
867            star = Some(pi);
868            mark = ti;
869            pi += 1;
870        } else if let Some(s) = star {
871            pi = s + 1;
872            mark += 1;
873            ti = mark;
874        } else {
875            return false;
876        }
877    }
878    p[pi..].iter().all(|c| *c == '%')
879}
880
881fn binary(op: BinaryOp, x: Value, y: Value) -> Value {
882    use std::cmp::Ordering;
883    let cmp = |o: fn(Ordering) -> bool| {
884        // Comparisons with Null are false.
885        if x.is_null() || y.is_null() { Value::Bool(false) } else { Value::Bool(o(x.compare(&y))) }
886    };
887    match op {
888        BinaryOp::Eq => {
889            if x.is_null() || y.is_null() {
890                return Value::Bool(x.is_null() && y.is_null());
891            }
892            Value::Bool(x.loose_eq(&y))
893        }
894        BinaryOp::Ne => match binary(BinaryOp::Eq, x, y) {
895            Value::Bool(b) => Value::Bool(!b),
896            v => v,
897        },
898        BinaryOp::Lt => cmp(Ordering::is_lt),
899        BinaryOp::Le => cmp(Ordering::is_le),
900        BinaryOp::Gt => cmp(Ordering::is_gt),
901        BinaryOp::Ge => cmp(Ordering::is_ge),
902        BinaryOp::Like => {
903            if x.is_null() || y.is_null() {
904                Value::Bool(false)
905            } else {
906                Value::Bool(like(&x.to_text(), &y.to_text()))
907            }
908        }
909        BinaryOp::Add => match (&x, &y) {
910            (Value::Null, _) | (_, Value::Null) => Value::Null,
911            // Text joins: 'Nº ' + [Number].
912            (Value::Text(_), _) | (_, Value::Text(_)) => Value::Text(x.to_text() + &y.to_text()),
913            (Value::Date(d), _) => y.as_number().map_or(Value::Null, |n| Value::Date(d.add_days(n as i64))),
914            _ => arith(&x, &y, |a, b| a + b),
915        },
916        BinaryOp::Sub => match (&x, &y) {
917            (Value::Date(a), Value::Date(b)) => Value::Number((a.days - b.days) as f64),
918            (Value::Date(d), _) => {
919                y.as_number().map_or(Value::Null, |n| Value::Date(d.add_days(-(n as i64))))
920            }
921            _ => arith(&x, &y, |a, b| a - b),
922        },
923        BinaryOp::Mul => arith(&x, &y, |a, b| a * b),
924        // Division by zero gives Null rather than infinity, so totals stay printable.
925        BinaryOp::Div => match y.as_number() {
926            Some(d) if d != 0.0 => arith(&x, &y, |a, b| a / b),
927            _ => Value::Null,
928        },
929        BinaryOp::Rem => match y.as_number() {
930            Some(d) if d != 0.0 => arith(&x, &y, |a, b| a % b),
931            _ => Value::Null,
932        },
933        BinaryOp::BitAnd => arith(&x, &y, |a, b| ((a as i64) & (b as i64)) as f64),
934        BinaryOp::BitOr => arith(&x, &y, |a, b| ((a as i64) | (b as i64)) as f64),
935        BinaryOp::BitXor => arith(&x, &y, |a, b| ((a as i64) ^ (b as i64)) as f64),
936    }
937}
938
939fn arith(x: &Value, y: &Value, f: impl Fn(f64, f64) -> f64) -> Value {
940    match (x.as_number(), y.as_number()) {
941        (Some(a), Some(b)) => Value::Number(f(a, b)),
942        _ => Value::Null,
943    }
944}