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    IsBlank,
69    IsNumber,
70    IsText,
71    IsLogical,
72}
73
74#[derive(Debug)]
75pub enum PreparedArg<'a> {
76    Value(Cow<'a, LiteralValue>),
77    Range(crate::engine::range_view::RangeView<'a>),
78    Reference(formualizer_parse::parser::ReferenceType),
79    Predicate(CriteriaPredicate),
80}
81
82pub struct PreparedArgs<'a> {
83    pub items: Vec<PreparedArg<'a>>,
84}
85
86#[derive(Default)]
87pub struct ValidationOptions {
88    pub warn_only: bool,
89}
90
91// Legacy adapter removed in clean break.
92
93pub fn parse_criteria(v: &LiteralValue) -> Result<CriteriaPredicate, ExcelError> {
94    match v {
95        LiteralValue::Text(s) => {
96            let s_trim = s.trim();
97
98            let unquote = |t: &str| -> String {
99                let t = t.trim();
100                if let Some(inner) = t.strip_prefix('"').and_then(|x| x.strip_suffix('"')) {
101                    inner.replace("\"\"", "\"")
102                } else {
103                    t.to_string()
104                }
105            };
106
107            // Operators: >=, <=, <>, >, <, =
108            let ops = [">=", "<=", "<>", ">", "<", "="];
109            for op in ops.iter() {
110                if let Some(rhs) = s_trim.strip_prefix(op) {
111                    let rhs_trim = rhs.trim();
112                    // Try numeric parse for comparisons
113                    if let Ok(n) = rhs_trim.parse::<f64>() {
114                        return Ok(match *op {
115                            ">=" => CriteriaPredicate::Ge(n),
116                            "<=" => CriteriaPredicate::Le(n),
117                            ">" => CriteriaPredicate::Gt(n),
118                            "<" => CriteriaPredicate::Lt(n),
119                            "=" => CriteriaPredicate::Eq(LiteralValue::Number(n)),
120                            "<>" => CriteriaPredicate::Ne(LiteralValue::Number(n)),
121                            _ => unreachable!(),
122                        });
123                    }
124                    // Fallback: non-numeric equals/neq text (support Excel-style quoted strings: ="aa")
125                    let lit = LiteralValue::Text(unquote(rhs_trim));
126                    return Ok(match *op {
127                        "=" => CriteriaPredicate::Eq(lit),
128                        "<>" => CriteriaPredicate::Ne(lit),
129                        ">=" | "<=" | ">" | "<" => {
130                            // Non-numeric compare: not fully supported; degrade to equality on full expression
131                            CriteriaPredicate::Eq(LiteralValue::Text(s_trim.to_string()))
132                        }
133                        _ => unreachable!(),
134                    });
135                }
136            }
137
138            let plain = unquote(s_trim);
139
140            // Wildcards * or ? => TextLike
141            if plain.contains('*') || plain.contains('?') {
142                return Ok(CriteriaPredicate::TextLike {
143                    pattern: plain,
144                    case_insensitive: true,
145                });
146            }
147            // Booleans TRUE/FALSE
148            let lower = plain.to_ascii_lowercase();
149            if lower == "true" {
150                return Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(true)));
151            } else if lower == "false" {
152                return Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(false)));
153            }
154            // Plain text equality
155            Ok(CriteriaPredicate::Eq(LiteralValue::Text(plain)))
156        }
157        LiteralValue::Empty => Ok(CriteriaPredicate::IsBlank),
158        LiteralValue::Number(n) => Ok(CriteriaPredicate::Eq(LiteralValue::Number(*n))),
159        // Normalize integer criteria to Number for Excel-style numeric coercions
160        // (e.g. blank == 0, numeric text == number, etc.)
161        LiteralValue::Int(i) => Ok(CriteriaPredicate::Eq(LiteralValue::Number(*i as f64))),
162        LiteralValue::Boolean(b) => Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(*b))),
163        LiteralValue::Error(e) => Err(e.clone()),
164        LiteralValue::Array(arr) => {
165            // Treat 1x1 array literals as scalars for criteria parsing
166            if arr.len() == 1 && arr.first().map(|r| r.len()).unwrap_or(0) == 1 {
167                parse_criteria(&arr[0][0])
168            } else {
169                Ok(CriteriaPredicate::Eq(LiteralValue::Array(arr.clone())))
170            }
171        }
172        other => Ok(CriteriaPredicate::Eq(other.clone())),
173    }
174}
175
176pub fn validate_and_prepare<'a, 'b>(
177    args: &'a [ArgumentHandle<'a, 'b>],
178    schema: &[ArgSchema],
179    options: ValidationOptions,
180) -> Result<PreparedArgs<'a>, ExcelError> {
181    // Arity: simple rule – if schema.len() == 1, allow variadic repetition; else match up to schema.len()
182    if schema.is_empty() {
183        return Ok(PreparedArgs { items: Vec::new() });
184    }
185
186    let mut items: Vec<PreparedArg<'a>> = Vec::with_capacity(args.len());
187    for (idx, arg) in args.iter().enumerate() {
188        let spec = if schema.len() == 1 {
189            &schema[0]
190        } else if idx < schema.len() {
191            &schema[idx]
192        } else {
193            // Attempt to find a repeating spec (e.g., variadic tail like CHOOSE, SUM, etc.)
194            if let Some(rep_spec) = schema.iter().find(|s| s.repeating.is_some()) {
195                rep_spec
196            } else if options.warn_only {
197                continue;
198            } else {
199                return Err(
200                    ExcelError::new(ExcelErrorKind::Value).with_message("Too many arguments")
201                );
202            }
203        };
204
205        // By-ref argument: require a reference (AST literal or function-returned)
206        if spec.by_ref {
207            match arg.as_reference_or_eval() {
208                Ok(r) => {
209                    items.push(PreparedArg::Reference(r));
210                    continue;
211                }
212                Err(e) => {
213                    if options.warn_only {
214                        continue;
215                    } else {
216                        return Err(e);
217                    }
218                }
219            }
220        }
221
222        // Criteria policy: parse into predicate
223        if matches!(spec.coercion, CoercionPolicy::Criteria) {
224            let v = arg.value()?.into_literal();
225            match parse_criteria(&v) {
226                Ok(pred) => {
227                    items.push(PreparedArg::Predicate(pred));
228                    continue;
229                }
230                Err(e) => {
231                    if options.warn_only {
232                        continue;
233                    } else {
234                        return Err(e);
235                    }
236                }
237            }
238        }
239
240        // Shape handling
241        match spec.shape {
242            ShapeKind::Scalar => {
243                // Collapse to scalar if needed (top-left for arrays)
244                match arg.value() {
245                    Ok(cv) => {
246                        let v: Cow<'_, LiteralValue> = match cv {
247                            crate::traits::CalcValue::Scalar(LiteralValue::Array(arr)) => {
248                                let tl = arr
249                                    .first()
250                                    .and_then(|row| row.first())
251                                    .cloned()
252                                    .unwrap_or(LiteralValue::Empty);
253                                Cow::Owned(tl)
254                            }
255                            crate::traits::CalcValue::Range(rv) => Cow::Owned(rv.get_cell(0, 0)),
256                            crate::traits::CalcValue::Scalar(s) => Cow::Owned(s),
257                            crate::traits::CalcValue::Callable(_) => {
258                                Cow::Owned(LiteralValue::Error(
259                                    ExcelError::new(ExcelErrorKind::Calc)
260                                        .with_message("LAMBDA value must be invoked"),
261                                ))
262                            }
263                        };
264                        // Apply coercion policy to Value shapes when applicable
265                        let coerced = match spec.coercion {
266                            CoercionPolicy::None => v,
267                            CoercionPolicy::NumberStrict => {
268                                match crate::coercion::to_number_strict(v.as_ref()) {
269                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
270                                    Err(e) => {
271                                        if options.warn_only {
272                                            v
273                                        } else {
274                                            return Err(e);
275                                        }
276                                    }
277                                }
278                            }
279                            CoercionPolicy::NumberLenientText => {
280                                match crate::coercion::to_number_lenient(v.as_ref()) {
281                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
282                                    Err(e) => {
283                                        if options.warn_only {
284                                            v
285                                        } else {
286                                            return Err(e);
287                                        }
288                                    }
289                                }
290                            }
291                            CoercionPolicy::Logical => {
292                                match crate::coercion::to_logical(v.as_ref()) {
293                                    Ok(b) => Cow::Owned(LiteralValue::Boolean(b)),
294                                    Err(e) => {
295                                        if options.warn_only {
296                                            v
297                                        } else {
298                                            return Err(e);
299                                        }
300                                    }
301                                }
302                            }
303                            CoercionPolicy::Criteria => v, // handled per-function currently
304                            CoercionPolicy::DateTimeSerial => {
305                                match crate::coercion::to_datetime_serial(v.as_ref()) {
306                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
307                                    Err(e) => {
308                                        if options.warn_only {
309                                            v
310                                        } else {
311                                            return Err(e);
312                                        }
313                                    }
314                                }
315                            }
316                        };
317                        items.push(PreparedArg::Value(coerced))
318                    }
319                    Err(e) => items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(e)))),
320                }
321            }
322            ShapeKind::Range | ShapeKind::Array => {
323                match arg.range_view() {
324                    Ok(r) => items.push(PreparedArg::Range(r)),
325                    Err(_e) => {
326                        // Excel-compatible: functions that accept ranges typically also accept scalars.
327                        // Fall back to treating the argument as a scalar value, even in strict mode.
328                        match arg.value() {
329                            Ok(v) => items.push(PreparedArg::Value(Cow::Owned(v.into_literal()))),
330                            Err(e2) => {
331                                items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(e2))))
332                            }
333                        }
334                    }
335                }
336            }
337        }
338    }
339
340    Ok(PreparedArgs { items })
341}