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