Skip to main content

formualizer_eval/
args.rs

1use crate::traits::ArgumentHandle;
2// Note: Validator no longer depends on EvaluationContext; keep it engine-agnostic.
3use formualizer_common::{ArgKind, ExcelError, ExcelErrorKind, LiteralValue};
4use smallvec::{SmallVec, smallvec};
5use std::borrow::Cow;
6
7#[derive(Copy, Clone, Debug, Eq, PartialEq)]
8pub enum ShapeKind {
9    Scalar,
10    Range,
11    Array,
12}
13
14pub use formualizer_common::CoercionPolicy;
15
16#[derive(Clone, Debug)]
17pub struct ArgSchema {
18    pub kinds: SmallVec<[ArgKind; 2]>,
19    pub required: bool,
20    pub by_ref: bool,
21    pub shape: ShapeKind,
22    pub coercion: CoercionPolicy,
23    pub max: Option<usize>,
24    pub repeating: Option<usize>,
25    pub default: Option<LiteralValue>,
26}
27
28impl ArgSchema {
29    pub fn any() -> Self {
30        Self {
31            kinds: smallvec![ArgKind::Any],
32            required: true,
33            by_ref: false,
34            shape: ShapeKind::Scalar,
35            coercion: CoercionPolicy::None,
36            max: None,
37            repeating: None,
38            default: None,
39        }
40    }
41
42    pub fn number_lenient_scalar() -> Self {
43        Self {
44            kinds: smallvec![ArgKind::Number],
45            required: true,
46            by_ref: false,
47            shape: ShapeKind::Scalar,
48            coercion: CoercionPolicy::NumberLenientText,
49            max: None,
50            repeating: None,
51            default: None,
52        }
53    }
54}
55
56#[derive(Clone, Debug)]
57pub enum CriteriaPredicate {
58    Eq(LiteralValue),
59    Ne(LiteralValue),
60    Gt(f64),
61    Ge(f64),
62    Lt(f64),
63    Le(f64),
64    TextLike {
65        pattern: String,
66        case_insensitive: bool,
67    },
68    /// `"<>a*"`: the complement of [`CriteriaPredicate::TextLike`]; numbers,
69    /// booleans, errors and blank cells match.
70    NotTextLike {
71        pattern: String,
72        case_insensitive: bool,
73    },
74    /// `">b"` and friends with a non-numeric operand: compares text cells
75    /// only (case-insensitive); other cells never match.
76    TextGt(String),
77    TextGe(String),
78    TextLt(String),
79    TextLe(String),
80    /// `"="`: a truly blank cell (not empty text).
81    IsBlank,
82    /// `"<>"`: any cell that is not truly blank (empty text matches).
83    NotBlank,
84    IsNumber,
85    IsText,
86    IsLogical,
87}
88
89#[derive(Debug)]
90pub enum PreparedArg<'a> {
91    Value(Cow<'a, LiteralValue>),
92    Range(crate::engine::range_view::RangeView<'a>),
93    Reference(formualizer_parse::parser::ReferenceType),
94    Predicate(CriteriaPredicate),
95}
96
97pub struct PreparedArgs<'a> {
98    pub items: Vec<PreparedArg<'a>>,
99}
100
101pub(crate) struct InlinePreparedArgs<'a> {
102    pub items: SmallVec<[PreparedArg<'a>; 4]>,
103}
104
105#[derive(Default)]
106pub struct ValidationOptions {
107    pub warn_only: bool,
108    /// Minimum number of arguments the function requires.  When non-zero,
109    /// `validate_and_prepare` rejects calls with fewer arguments before any
110    /// per-argument validation runs, preventing out-of-bounds panics in
111    /// `eval` implementations.
112    pub min_args: usize,
113}
114
115// Legacy adapter removed in clean break.
116
117/// Parse a criteria argument (COUNTIF/SUMIF/...IFS, MAXIFS/MINIFS and the
118/// database functions) into a predicate, following Excel's rules:
119/// - a number, boolean or error criterion matches that value;
120/// - a reference to an empty cell is the number 0 (blank cells do not match);
121/// - `""` matches blank cells and empty text, `"="` only blank cells, and
122///   `"<>"` every cell that is not blank;
123/// - `?`, `*` and `~` are wildcards, matched against text cells only, with or
124///   without a leading `=` (`"<>a*"` is the complement);
125/// - `>`, `>=`, `<`, `<=` compare numbers with a numeric operand and text
126///   with a text operand.
127pub fn parse_criteria(v: &LiteralValue) -> Result<CriteriaPredicate, ExcelError> {
128    match v {
129        LiteralValue::Text(s) => {
130            // Leading spaces before an operator are tolerated; the text operand
131            // itself is matched exactly, so trailing spaces are significant
132            // (`"Ltd "` does not match `"Ltd"`).
133            let s_trim = s.trim_start();
134
135            let unquote = |t: &str| -> String {
136                let q = t.trim();
137                if let Some(inner) = q.strip_prefix('"').and_then(|x| x.strip_suffix('"')) {
138                    inner.replace("\"\"", "\"")
139                } else {
140                    t.to_string()
141                }
142            };
143            let is_pattern = |t: &str| t.contains('*') || t.contains('?') || t.contains("~~");
144
145            // Operators: >=, <=, <>, >, <, =
146            let ops = [">=", "<=", "<>", ">", "<", "="];
147            for op in ops.iter() {
148                if let Some(rhs) = s_trim.strip_prefix(op) {
149                    // Numbers, then date/time text (`">=2/1/02"`), as Excel
150                    // reads the operand the way it reads typed input.
151                    if let Some(n) = parse_criteria_number(rhs.trim())
152                        .or_else(|| parse_criteria_datetime(rhs.trim()))
153                    {
154                        return Ok(match *op {
155                            ">=" => CriteriaPredicate::Ge(n),
156                            "<=" => CriteriaPredicate::Le(n),
157                            ">" => CriteriaPredicate::Gt(n),
158                            "<" => CriteriaPredicate::Lt(n),
159                            "=" => CriteriaPredicate::Eq(LiteralValue::Number(n)),
160                            "<>" => CriteriaPredicate::Ne(LiteralValue::Number(n)),
161                            _ => unreachable!(),
162                        });
163                    }
164                    // Fallback: non-numeric operand (support Excel-style quoted strings: ="aa")
165                    let text = unquote(rhs);
166                    let boolean = match text.to_ascii_lowercase().as_str() {
167                        "true" => Some(true),
168                        "false" => Some(false),
169                        _ => None,
170                    };
171                    return Ok(match *op {
172                        "=" if text.is_empty() => CriteriaPredicate::IsBlank,
173                        "<>" if text.is_empty() => CriteriaPredicate::NotBlank,
174                        "=" if is_pattern(&text) => CriteriaPredicate::TextLike {
175                            pattern: text,
176                            case_insensitive: true,
177                        },
178                        "<>" if is_pattern(&text) => CriteriaPredicate::NotTextLike {
179                            pattern: text,
180                            case_insensitive: true,
181                        },
182                        "=" => match boolean {
183                            Some(b) => CriteriaPredicate::Eq(LiteralValue::Boolean(b)),
184                            None => CriteriaPredicate::Eq(LiteralValue::Text(text)),
185                        },
186                        "<>" => match boolean {
187                            Some(b) => CriteriaPredicate::Ne(LiteralValue::Boolean(b)),
188                            None => CriteriaPredicate::Ne(LiteralValue::Text(text)),
189                        },
190                        ">=" => CriteriaPredicate::TextGe(text),
191                        "<=" => CriteriaPredicate::TextLe(text),
192                        ">" => CriteriaPredicate::TextGt(text),
193                        "<" => CriteriaPredicate::TextLt(text),
194                        _ => unreachable!(),
195                    });
196                }
197            }
198
199            let plain = unquote(s);
200
201            // Wildcards or escaped tilde => TextLike (including literal ~* and ~?).
202            if is_pattern(&plain) {
203                return Ok(CriteriaPredicate::TextLike {
204                    pattern: plain,
205                    case_insensitive: true,
206                });
207            }
208            // Booleans TRUE/FALSE
209            let lower = plain.to_ascii_lowercase();
210            if lower == "true" {
211                return Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(true)));
212            } else if lower == "false" {
213                return Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(false)));
214            }
215            // Date or time text (`"2/1/2002"`) matches the date's serial.
216            if parse_criteria_number(plain.trim()).is_none()
217                && let Some(serial) = parse_criteria_datetime(plain.trim())
218            {
219                return Ok(CriteriaPredicate::Eq(LiteralValue::Number(serial)));
220            }
221            // Plain text equality
222            Ok(CriteriaPredicate::Eq(LiteralValue::Text(plain)))
223        }
224        // Excel: a criteria argument that references an empty cell is 0.
225        LiteralValue::Empty => Ok(CriteriaPredicate::Eq(LiteralValue::Number(0.0))),
226        LiteralValue::Number(n) => Ok(CriteriaPredicate::Eq(LiteralValue::Number(*n))),
227        // Normalize integer criteria to Number for Excel-style numeric coercions
228        // (numeric text == number, etc.)
229        LiteralValue::Int(i) => Ok(CriteriaPredicate::Eq(LiteralValue::Number(*i as f64))),
230        LiteralValue::Boolean(b) => Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(*b))),
231        LiteralValue::Error(e) => Err(e.clone()),
232        LiteralValue::Array(arr) => {
233            // Treat 1x1 array literals as scalars for criteria parsing
234            if arr.len() == 1 && arr.first().map(|r| r.len()).unwrap_or(0) == 1 {
235                parse_criteria(&arr[0][0])
236            } else {
237                Ok(CriteriaPredicate::Eq(LiteralValue::Array(arr.clone())))
238            }
239        }
240        other => Ok(CriteriaPredicate::Eq(other.clone())),
241    }
242}
243
244/// A date or time operand (`"2/1/02"`, `"1-Feb-2002"`, `"13:30"`) as a 1900-system
245/// serial. Criteria parsing has no workbook context, so 1904-system workbooks
246/// would see the 1900 serial.
247fn parse_criteria_datetime(text: &str) -> Option<f64> {
248    if text.is_empty() || !text.bytes().any(|b| b.is_ascii_digit()) {
249        return None;
250    }
251    formualizer_common::parse_excel_datetime_text_to_serial_for(
252        formualizer_common::DateSystem::Excel1900,
253        text,
254    )
255}
256
257/// A finite number operand of a criteria operator (`">=1e3"`, `"<>-2"`).
258fn parse_criteria_number(text: &str) -> Option<f64> {
259    text.parse::<f64>().ok().filter(|n| n.is_finite())
260}
261
262pub fn validate_and_prepare<'a, 'b>(
263    args: &'a [ArgumentHandle<'a, 'b>],
264    schema: &[ArgSchema],
265    options: ValidationOptions,
266) -> Result<PreparedArgs<'a>, ExcelError> {
267    validate_and_prepare_inline(args, schema, options).map(|prepared| PreparedArgs {
268        items: prepared.items.into_vec(),
269    })
270}
271
272/// Dispatch keeps up to four prepared arguments on the stack. The public
273/// adapter retains its Vec-based API for consumers that need the prepared data.
274pub(crate) fn validate_and_prepare_inline<'a, 'b>(
275    args: &'a [ArgumentHandle<'a, 'b>],
276    schema: &[ArgSchema],
277    options: ValidationOptions,
278) -> Result<InlinePreparedArgs<'a>, ExcelError> {
279    // Minimum arity — reject too-few arguments before per-arg validation so
280    // that individual `eval` implementations cannot panic on indexing.
281    if options.min_args > 0 && args.len() < options.min_args {
282        if options.warn_only {
283            return Ok(InlinePreparedArgs {
284                items: SmallVec::new(),
285            });
286        }
287        return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
288            "Too few arguments: expected at least {}, got {}",
289            options.min_args,
290            args.len()
291        )));
292    }
293
294    // Arity: simple rule – if schema.len() == 1, allow variadic repetition; else match up to schema.len()
295    if schema.is_empty() {
296        return Ok(InlinePreparedArgs {
297            items: SmallVec::new(),
298        });
299    }
300
301    let mut items: SmallVec<[PreparedArg<'a>; 4]> = SmallVec::with_capacity(args.len());
302    for (idx, arg) in args.iter().enumerate() {
303        let spec = if schema.len() == 1 {
304            &schema[0]
305        } else if idx < schema.len() {
306            &schema[idx]
307        } else {
308            // Attempt to find a repeating spec (e.g., variadic tail like CHOOSE, SUM, etc.)
309            if let Some(rep_spec) = schema.iter().find(|s| s.repeating.is_some()) {
310                rep_spec
311            } else if options.warn_only {
312                continue;
313            } else {
314                return Err(
315                    ExcelError::new(ExcelErrorKind::Value).with_message("Too many arguments")
316                );
317            }
318        };
319
320        // By-ref argument: require a reference (AST literal or function-returned)
321        if spec.by_ref {
322            match arg.as_reference_or_eval() {
323                Ok(r) => {
324                    items.push(PreparedArg::Reference(r));
325                    continue;
326                }
327                Err(e) => {
328                    if options.warn_only {
329                        continue;
330                    } else {
331                        return Err(e);
332                    }
333                }
334            }
335        }
336
337        // Criteria policy: parse into predicate
338        if matches!(spec.coercion, CoercionPolicy::Criteria) {
339            let v = arg.value()?.into_literal();
340            match parse_criteria(&v) {
341                Ok(pred) => {
342                    items.push(PreparedArg::Predicate(pred));
343                    continue;
344                }
345                Err(e) => {
346                    if options.warn_only {
347                        continue;
348                    } else {
349                        return Err(e);
350                    }
351                }
352            }
353        }
354
355        // Shape handling
356        match spec.shape {
357            ShapeKind::Scalar => {
358                // Collapse to scalar if needed (top-left for arrays)
359                match arg.value() {
360                    Ok(cv) => {
361                        let v: Cow<'_, LiteralValue> = match cv {
362                            crate::traits::CalcValue::Scalar(LiteralValue::Array(arr)) => {
363                                let tl = arr
364                                    .first()
365                                    .and_then(|row| row.first())
366                                    .cloned()
367                                    .unwrap_or(LiteralValue::Empty);
368                                Cow::Owned(tl)
369                            }
370                            crate::traits::CalcValue::Range(rv) => Cow::Owned(rv.get_cell(0, 0)),
371                            crate::traits::CalcValue::Scalar(s)
372                            | crate::traits::CalcValue::AnnotatedScalar(s, _) => Cow::Owned(s),
373                            crate::traits::CalcValue::Callable(_) => {
374                                Cow::Owned(LiteralValue::Error(
375                                    ExcelError::new(ExcelErrorKind::Calc)
376                                        .with_message("LAMBDA value must be invoked"),
377                                ))
378                            }
379                        };
380                        // Apply coercion policy to Value shapes when applicable
381                        let coerced = match spec.coercion {
382                            CoercionPolicy::None => v,
383                            CoercionPolicy::NumberStrict => {
384                                match crate::coercion::to_number_strict(v.as_ref()) {
385                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
386                                    Err(e) => {
387                                        if options.warn_only {
388                                            v
389                                        } else {
390                                            return Err(e);
391                                        }
392                                    }
393                                }
394                            }
395                            CoercionPolicy::NumberLenientText => {
396                                match crate::coercion::to_number_lenient(v.as_ref()) {
397                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
398                                    Err(e) => {
399                                        if options.warn_only {
400                                            v
401                                        } else {
402                                            return Err(e);
403                                        }
404                                    }
405                                }
406                            }
407                            CoercionPolicy::Logical => {
408                                match crate::coercion::to_logical(v.as_ref()) {
409                                    Ok(b) => Cow::Owned(LiteralValue::Boolean(b)),
410                                    Err(e) => {
411                                        if options.warn_only {
412                                            v
413                                        } else {
414                                            return Err(e);
415                                        }
416                                    }
417                                }
418                            }
419                            CoercionPolicy::Criteria => v, // handled per-function currently
420                            CoercionPolicy::DateTimeSerial => {
421                                match crate::coercion::to_datetime_serial(v.as_ref()) {
422                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
423                                    Err(e) => {
424                                        if options.warn_only {
425                                            v
426                                        } else {
427                                            return Err(e);
428                                        }
429                                    }
430                                }
431                            }
432                        };
433                        items.push(PreparedArg::Value(coerced))
434                    }
435                    Err(e) => items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(e)))),
436                }
437            }
438            ShapeKind::Range | ShapeKind::Array => match arg.resolve_once() {
439                Ok(crate::traits::ResolvedArgument::Range(range))
440                | Ok(crate::traits::ResolvedArgument::Value(crate::traits::CalcValue::Range(
441                    range,
442                ))) => items.push(PreparedArg::Range(range)),
443                Ok(crate::traits::ResolvedArgument::Value(value)) => {
444                    // Excel-compatible: range-accepting functions also accept scalars.
445                    items.push(PreparedArg::Value(Cow::Owned(value.into_literal())))
446                }
447                Ok(crate::traits::ResolvedArgument::ReferenceError(error)) => {
448                    items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(error))))
449                }
450                Err(error) => {
451                    items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(error))))
452                }
453            },
454        }
455    }
456
457    Ok(InlinePreparedArgs { items })
458}