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;
127/// - an error value, or the text of one (`"#N/A"`, `"=#DIV/0!"`), matches
128///   cells holding that error (`"<>#N/A"` every other cell).
129pub fn parse_criteria(v: &LiteralValue) -> Result<CriteriaPredicate, ExcelError> {
130    match v {
131        LiteralValue::Text(s) => {
132            // Leading spaces before an operator are tolerated; the text operand
133            // itself is matched exactly, so trailing spaces are significant
134            // (`"Ltd "` does not match `"Ltd"`).
135            let s_trim = s.trim_start();
136
137            let unquote = |t: &str| -> String {
138                let q = t.trim();
139                if let Some(inner) = q.strip_prefix('"').and_then(|x| x.strip_suffix('"')) {
140                    inner.replace("\"\"", "\"")
141                } else {
142                    t.to_string()
143                }
144            };
145            let is_pattern = |t: &str| t.contains('*') || t.contains('?') || t.contains("~~");
146
147            // Operators: >=, <=, <>, >, <, =
148            let ops = [">=", "<=", "<>", ">", "<", "="];
149            for op in ops.iter() {
150                if let Some(rhs) = s_trim.strip_prefix(op) {
151                    // Numbers, then date/time text (`">=2/1/02"`), as Excel
152                    // reads the operand the way it reads typed input.
153                    if let Some(n) = parse_criteria_number(rhs.trim())
154                        .or_else(|| parse_criteria_datetime(rhs.trim()))
155                    {
156                        return Ok(match *op {
157                            ">=" => CriteriaPredicate::Ge(n),
158                            "<=" => CriteriaPredicate::Le(n),
159                            ">" => CriteriaPredicate::Gt(n),
160                            "<" => CriteriaPredicate::Lt(n),
161                            "=" => CriteriaPredicate::Eq(LiteralValue::Number(n)),
162                            "<>" => CriteriaPredicate::Ne(LiteralValue::Number(n)),
163                            _ => unreachable!(),
164                        });
165                    }
166                    // Fallback: non-numeric operand (support Excel-style quoted strings: ="aa")
167                    let text = unquote(rhs);
168                    if let Some(error) = error_operand(&text) {
169                        match *op {
170                            "=" => return Ok(CriteriaPredicate::Eq(error)),
171                            "<>" => return Ok(CriteriaPredicate::Ne(error)),
172                            _ => {}
173                        }
174                    }
175                    let boolean = match text.to_ascii_lowercase().as_str() {
176                        "true" => Some(true),
177                        "false" => Some(false),
178                        _ => None,
179                    };
180                    return Ok(match *op {
181                        "=" if text.is_empty() => CriteriaPredicate::IsBlank,
182                        "<>" if text.is_empty() => CriteriaPredicate::NotBlank,
183                        "=" if is_pattern(&text) => CriteriaPredicate::TextLike {
184                            pattern: text,
185                            case_insensitive: true,
186                        },
187                        "<>" if is_pattern(&text) => CriteriaPredicate::NotTextLike {
188                            pattern: text,
189                            case_insensitive: true,
190                        },
191                        "=" => match boolean {
192                            Some(b) => CriteriaPredicate::Eq(LiteralValue::Boolean(b)),
193                            None => CriteriaPredicate::Eq(LiteralValue::Text(text)),
194                        },
195                        "<>" => match boolean {
196                            Some(b) => CriteriaPredicate::Ne(LiteralValue::Boolean(b)),
197                            None => CriteriaPredicate::Ne(LiteralValue::Text(text)),
198                        },
199                        ">=" => CriteriaPredicate::TextGe(text),
200                        "<=" => CriteriaPredicate::TextLe(text),
201                        ">" => CriteriaPredicate::TextGt(text),
202                        "<" => CriteriaPredicate::TextLt(text),
203                        _ => unreachable!(),
204                    });
205                }
206            }
207
208            let plain = unquote(s);
209            if let Some(error) = error_operand(&plain) {
210                return Ok(CriteriaPredicate::Eq(error));
211            }
212
213            // Wildcards or escaped tilde => TextLike (including literal ~* and ~?).
214            if is_pattern(&plain) {
215                return Ok(CriteriaPredicate::TextLike {
216                    pattern: plain,
217                    case_insensitive: true,
218                });
219            }
220            // Booleans TRUE/FALSE
221            let lower = plain.to_ascii_lowercase();
222            if lower == "true" {
223                return Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(true)));
224            } else if lower == "false" {
225                return Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(false)));
226            }
227            // Date or time text (`"2/1/2002"`) matches the date's serial.
228            if parse_criteria_number(plain.trim()).is_none()
229                && let Some(serial) = parse_criteria_datetime(plain.trim())
230            {
231                return Ok(CriteriaPredicate::Eq(LiteralValue::Number(serial)));
232            }
233            // Plain text equality
234            Ok(CriteriaPredicate::Eq(LiteralValue::Text(plain)))
235        }
236        // Excel: a criteria argument that references an empty cell is 0.
237        LiteralValue::Empty => Ok(CriteriaPredicate::Eq(LiteralValue::Number(0.0))),
238        LiteralValue::Number(n) => Ok(CriteriaPredicate::Eq(LiteralValue::Number(*n))),
239        // Normalize integer criteria to Number for Excel-style numeric coercions
240        // (numeric text == number, etc.)
241        LiteralValue::Int(i) => Ok(CriteriaPredicate::Eq(LiteralValue::Number(*i as f64))),
242        LiteralValue::Boolean(b) => Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(*b))),
243        // An error criterion matches cells holding the same error.
244        LiteralValue::Error(e) => Ok(CriteriaPredicate::Eq(LiteralValue::Error(e.clone()))),
245        LiteralValue::Array(arr) => {
246            // Treat 1x1 array literals as scalars for criteria parsing
247            if arr.len() == 1 && arr.first().map(|r| r.len()).unwrap_or(0) == 1 {
248                parse_criteria(&arr[0][0])
249            } else {
250                Ok(CriteriaPredicate::Eq(LiteralValue::Array(arr.clone())))
251            }
252        }
253        other => Ok(CriteriaPredicate::Eq(other.clone())),
254    }
255}
256
257/// The error a criteria operand spells exactly (`"#N/A"`, `"#div/0!"`).
258fn error_operand(text: &str) -> Option<LiteralValue> {
259    if text.trim() != text {
260        return None;
261    }
262    formualizer_common::ExcelErrorKind::try_parse(text)
263        .map(|kind| LiteralValue::Error(ExcelError::new(kind)))
264}
265
266/// A date or time operand (`"2/1/02"`, `"1-Feb-2002"`, `"13:30"`) as a 1900-system
267/// serial. Criteria parsing has no workbook context, so 1904-system workbooks
268/// would see the 1900 serial.
269fn parse_criteria_datetime(text: &str) -> Option<f64> {
270    if text.is_empty() || !text.bytes().any(|b| b.is_ascii_digit()) {
271        return None;
272    }
273    formualizer_common::parse_excel_datetime_text_to_serial_for(
274        formualizer_common::DateSystem::Excel1900,
275        text,
276    )
277}
278
279/// A finite number operand of a criteria operator (`">=1e3"`, `"<>-2"`).
280fn parse_criteria_number(text: &str) -> Option<f64> {
281    text.parse::<f64>().ok().filter(|n| n.is_finite())
282}
283
284pub fn validate_and_prepare<'a, 'b>(
285    args: &'a [ArgumentHandle<'a, 'b>],
286    schema: &[ArgSchema],
287    options: ValidationOptions,
288) -> Result<PreparedArgs<'a>, ExcelError> {
289    validate_and_prepare_inline(args, schema, options).map(|prepared| PreparedArgs {
290        items: prepared.items.into_vec(),
291    })
292}
293
294/// Dispatch keeps up to four prepared arguments on the stack. The public
295/// adapter retains its Vec-based API for consumers that need the prepared data.
296pub(crate) fn validate_and_prepare_inline<'a, 'b>(
297    args: &'a [ArgumentHandle<'a, 'b>],
298    schema: &[ArgSchema],
299    options: ValidationOptions,
300) -> Result<InlinePreparedArgs<'a>, ExcelError> {
301    // Minimum arity — reject too-few arguments before per-arg validation so
302    // that individual `eval` implementations cannot panic on indexing.
303    if options.min_args > 0 && args.len() < options.min_args {
304        if options.warn_only {
305            return Ok(InlinePreparedArgs {
306                items: SmallVec::new(),
307            });
308        }
309        return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
310            "Too few arguments: expected at least {}, got {}",
311            options.min_args,
312            args.len()
313        )));
314    }
315
316    // Arity: simple rule – if schema.len() == 1, allow variadic repetition; else match up to schema.len()
317    if schema.is_empty() {
318        return Ok(InlinePreparedArgs {
319            items: SmallVec::new(),
320        });
321    }
322
323    let mut items: SmallVec<[PreparedArg<'a>; 4]> = SmallVec::with_capacity(args.len());
324    for (idx, arg) in args.iter().enumerate() {
325        let spec = if schema.len() == 1 {
326            &schema[0]
327        } else if idx < schema.len() {
328            &schema[idx]
329        } else {
330            // Attempt to find a repeating spec (e.g., variadic tail like CHOOSE, SUM, etc.)
331            if let Some(rep_spec) = schema.iter().find(|s| s.repeating.is_some()) {
332                rep_spec
333            } else if options.warn_only {
334                continue;
335            } else {
336                return Err(
337                    ExcelError::new(ExcelErrorKind::Value).with_message("Too many arguments")
338                );
339            }
340        };
341
342        // By-ref argument: require a reference (AST literal or function-returned)
343        if spec.by_ref {
344            match arg.as_reference_or_eval() {
345                Ok(r) => {
346                    items.push(PreparedArg::Reference(r));
347                    continue;
348                }
349                Err(e) => {
350                    if options.warn_only {
351                        continue;
352                    } else {
353                        return Err(e);
354                    }
355                }
356            }
357        }
358
359        // Criteria policy: parse into predicate
360        if matches!(spec.coercion, CoercionPolicy::Criteria) {
361            let v = arg.value()?.into_literal();
362            match parse_criteria(&v) {
363                Ok(pred) => {
364                    items.push(PreparedArg::Predicate(pred));
365                    continue;
366                }
367                Err(e) => {
368                    if options.warn_only {
369                        continue;
370                    } else {
371                        return Err(e);
372                    }
373                }
374            }
375        }
376
377        // Shape handling
378        match spec.shape {
379            ShapeKind::Scalar => {
380                // Collapse to scalar if needed (top-left for arrays)
381                match arg.value() {
382                    Ok(cv) => {
383                        let v: Cow<'_, LiteralValue> = match cv {
384                            crate::traits::CalcValue::Scalar(LiteralValue::Array(arr)) => {
385                                let tl = arr
386                                    .first()
387                                    .and_then(|row| row.first())
388                                    .cloned()
389                                    .unwrap_or(LiteralValue::Empty);
390                                Cow::Owned(tl)
391                            }
392                            crate::traits::CalcValue::Range(rv) => Cow::Owned(rv.get_cell(0, 0)),
393                            crate::traits::CalcValue::Scalar(s)
394                            | crate::traits::CalcValue::AnnotatedScalar(s, _) => Cow::Owned(s),
395                            crate::traits::CalcValue::Callable(_) => {
396                                Cow::Owned(LiteralValue::Error(
397                                    ExcelError::new(ExcelErrorKind::Calc)
398                                        .with_message("LAMBDA value must be invoked"),
399                                ))
400                            }
401                        };
402                        // Apply coercion policy to Value shapes when applicable
403                        let coerced = match spec.coercion {
404                            CoercionPolicy::None => v,
405                            CoercionPolicy::NumberStrict => {
406                                match crate::coercion::to_number_strict(v.as_ref()) {
407                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
408                                    Err(e) => {
409                                        if options.warn_only {
410                                            v
411                                        } else {
412                                            return Err(e);
413                                        }
414                                    }
415                                }
416                            }
417                            CoercionPolicy::NumberLenientText => {
418                                match crate::coercion::to_number_lenient(v.as_ref()) {
419                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
420                                    Err(e) => {
421                                        if options.warn_only {
422                                            v
423                                        } else {
424                                            return Err(e);
425                                        }
426                                    }
427                                }
428                            }
429                            CoercionPolicy::Logical => {
430                                match crate::coercion::to_logical(v.as_ref()) {
431                                    Ok(b) => Cow::Owned(LiteralValue::Boolean(b)),
432                                    Err(e) => {
433                                        if options.warn_only {
434                                            v
435                                        } else {
436                                            return Err(e);
437                                        }
438                                    }
439                                }
440                            }
441                            CoercionPolicy::Criteria => v, // handled per-function currently
442                            CoercionPolicy::DateTimeSerial => {
443                                match crate::coercion::to_datetime_serial(v.as_ref()) {
444                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
445                                    Err(e) => {
446                                        if options.warn_only {
447                                            v
448                                        } else {
449                                            return Err(e);
450                                        }
451                                    }
452                                }
453                            }
454                        };
455                        items.push(PreparedArg::Value(coerced))
456                    }
457                    Err(e) => items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(e)))),
458                }
459            }
460            ShapeKind::Range | ShapeKind::Array => match arg.resolve_once() {
461                Ok(crate::traits::ResolvedArgument::Range(range))
462                | Ok(crate::traits::ResolvedArgument::Value(crate::traits::CalcValue::Range(
463                    range,
464                ))) => items.push(PreparedArg::Range(range)),
465                Ok(crate::traits::ResolvedArgument::Value(value)) => {
466                    // Excel-compatible: range-accepting functions also accept scalars.
467                    items.push(PreparedArg::Value(Cow::Owned(value.into_literal())))
468                }
469                Ok(crate::traits::ResolvedArgument::ReferenceError(error)) => {
470                    items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(error))))
471                }
472                Err(error) => {
473                    items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(error))))
474                }
475            },
476        }
477    }
478
479    Ok(InlinePreparedArgs { items })
480}