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        LiteralValue::Int(i) => Ok(CriteriaPredicate::Eq(LiteralValue::Int(*i))),
160        LiteralValue::Boolean(b) => Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(*b))),
161        LiteralValue::Error(e) => Err(e.clone()),
162        LiteralValue::Array(arr) => {
163            // Treat 1x1 array literals as scalars for criteria parsing
164            if arr.len() == 1 && arr.first().map(|r| r.len()).unwrap_or(0) == 1 {
165                parse_criteria(&arr[0][0])
166            } else {
167                Ok(CriteriaPredicate::Eq(LiteralValue::Array(arr.clone())))
168            }
169        }
170        other => Ok(CriteriaPredicate::Eq(other.clone())),
171    }
172}
173
174pub fn validate_and_prepare<'a, 'b>(
175    args: &'a [ArgumentHandle<'a, 'b>],
176    schema: &[ArgSchema],
177    options: ValidationOptions,
178) -> Result<PreparedArgs<'a>, ExcelError> {
179    // Arity: simple rule – if schema.len() == 1, allow variadic repetition; else match up to schema.len()
180    if schema.is_empty() {
181        return Ok(PreparedArgs { items: Vec::new() });
182    }
183
184    let mut items: Vec<PreparedArg<'a>> = Vec::with_capacity(args.len());
185    for (idx, arg) in args.iter().enumerate() {
186        let spec = if schema.len() == 1 {
187            &schema[0]
188        } else if idx < schema.len() {
189            &schema[idx]
190        } else {
191            // Attempt to find a repeating spec (e.g., variadic tail like CHOOSE, SUM, etc.)
192            if let Some(rep_spec) = schema.iter().find(|s| s.repeating.is_some()) {
193                rep_spec
194            } else if options.warn_only {
195                continue;
196            } else {
197                return Err(
198                    ExcelError::new(ExcelErrorKind::Value).with_message("Too many arguments")
199                );
200            }
201        };
202
203        // By-ref argument: require a reference (AST literal or function-returned)
204        if spec.by_ref {
205            match arg.as_reference_or_eval() {
206                Ok(r) => {
207                    items.push(PreparedArg::Reference(r));
208                    continue;
209                }
210                Err(e) => {
211                    if options.warn_only {
212                        continue;
213                    } else {
214                        return Err(e);
215                    }
216                }
217            }
218        }
219
220        // Criteria policy: parse into predicate
221        if matches!(spec.coercion, CoercionPolicy::Criteria) {
222            let v = arg.value()?.into_literal();
223            match parse_criteria(&v) {
224                Ok(pred) => {
225                    items.push(PreparedArg::Predicate(pred));
226                    continue;
227                }
228                Err(e) => {
229                    if options.warn_only {
230                        continue;
231                    } else {
232                        return Err(e);
233                    }
234                }
235            }
236        }
237
238        // Shape handling
239        match spec.shape {
240            ShapeKind::Scalar => {
241                // Collapse to scalar if needed (top-left for arrays)
242                match arg.value() {
243                    Ok(cv) => {
244                        let v: Cow<'_, LiteralValue> = match cv {
245                            crate::traits::CalcValue::Scalar(LiteralValue::Array(arr)) => {
246                                let tl = arr
247                                    .first()
248                                    .and_then(|row| row.first())
249                                    .cloned()
250                                    .unwrap_or(LiteralValue::Empty);
251                                Cow::Owned(tl)
252                            }
253                            crate::traits::CalcValue::Range(rv) => Cow::Owned(rv.get_cell(0, 0)),
254                            crate::traits::CalcValue::Scalar(s) => Cow::Owned(s),
255                        };
256                        // Apply coercion policy to Value shapes when applicable
257                        let coerced = match spec.coercion {
258                            CoercionPolicy::None => v,
259                            CoercionPolicy::NumberStrict => {
260                                match crate::coercion::to_number_strict(v.as_ref()) {
261                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
262                                    Err(e) => {
263                                        if options.warn_only {
264                                            v
265                                        } else {
266                                            return Err(e);
267                                        }
268                                    }
269                                }
270                            }
271                            CoercionPolicy::NumberLenientText => {
272                                match crate::coercion::to_number_lenient(v.as_ref()) {
273                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
274                                    Err(e) => {
275                                        if options.warn_only {
276                                            v
277                                        } else {
278                                            return Err(e);
279                                        }
280                                    }
281                                }
282                            }
283                            CoercionPolicy::Logical => {
284                                match crate::coercion::to_logical(v.as_ref()) {
285                                    Ok(b) => Cow::Owned(LiteralValue::Boolean(b)),
286                                    Err(e) => {
287                                        if options.warn_only {
288                                            v
289                                        } else {
290                                            return Err(e);
291                                        }
292                                    }
293                                }
294                            }
295                            CoercionPolicy::Criteria => v, // handled per-function currently
296                            CoercionPolicy::DateTimeSerial => {
297                                match crate::coercion::to_datetime_serial(v.as_ref()) {
298                                    Ok(n) => Cow::Owned(LiteralValue::Number(n)),
299                                    Err(e) => {
300                                        if options.warn_only {
301                                            v
302                                        } else {
303                                            return Err(e);
304                                        }
305                                    }
306                                }
307                            }
308                        };
309                        items.push(PreparedArg::Value(coerced))
310                    }
311                    Err(e) => items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(e)))),
312                }
313            }
314            ShapeKind::Range | ShapeKind::Array => {
315                match arg.range_view() {
316                    Ok(r) => items.push(PreparedArg::Range(r)),
317                    Err(_e) => {
318                        // Excel-compatible: functions that accept ranges typically also accept scalars.
319                        // Fall back to treating the argument as a scalar value, even in strict mode.
320                        match arg.value() {
321                            Ok(v) => items.push(PreparedArg::Value(Cow::Owned(v.into_literal()))),
322                            Err(e2) => {
323                                items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(e2))))
324                            }
325                        }
326                    }
327                }
328            }
329        }
330    }
331
332    Ok(PreparedArgs { items })
333}