1use crate::traits::ArgumentHandle;
2use 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 pub min_args: usize,
94}
95
96pub fn parse_criteria(v: &LiteralValue) -> Result<CriteriaPredicate, ExcelError> {
99 match v {
100 LiteralValue::Text(s) => {
101 let s_trim = s.trim();
102
103 let unquote = |t: &str| -> String {
104 let t = t.trim();
105 if let Some(inner) = t.strip_prefix('"').and_then(|x| x.strip_suffix('"')) {
106 inner.replace("\"\"", "\"")
107 } else {
108 t.to_string()
109 }
110 };
111
112 let ops = [">=", "<=", "<>", ">", "<", "="];
114 for op in ops.iter() {
115 if let Some(rhs) = s_trim.strip_prefix(op) {
116 let rhs_trim = rhs.trim();
117 if let Ok(n) = rhs_trim.parse::<f64>() {
119 return Ok(match *op {
120 ">=" => CriteriaPredicate::Ge(n),
121 "<=" => CriteriaPredicate::Le(n),
122 ">" => CriteriaPredicate::Gt(n),
123 "<" => CriteriaPredicate::Lt(n),
124 "=" => CriteriaPredicate::Eq(LiteralValue::Number(n)),
125 "<>" => CriteriaPredicate::Ne(LiteralValue::Number(n)),
126 _ => unreachable!(),
127 });
128 }
129 let lit = LiteralValue::Text(unquote(rhs_trim));
131 return Ok(match *op {
132 "=" => CriteriaPredicate::Eq(lit),
133 "<>" => CriteriaPredicate::Ne(lit),
134 ">=" | "<=" | ">" | "<" => {
135 CriteriaPredicate::Eq(LiteralValue::Text(s_trim.to_string()))
137 }
138 _ => unreachable!(),
139 });
140 }
141 }
142
143 let plain = unquote(s_trim);
144
145 if plain.contains('*') || plain.contains('?') {
147 return Ok(CriteriaPredicate::TextLike {
148 pattern: plain,
149 case_insensitive: true,
150 });
151 }
152 let lower = plain.to_ascii_lowercase();
154 if lower == "true" {
155 return Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(true)));
156 } else if lower == "false" {
157 return Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(false)));
158 }
159 Ok(CriteriaPredicate::Eq(LiteralValue::Text(plain)))
161 }
162 LiteralValue::Empty => Ok(CriteriaPredicate::IsBlank),
163 LiteralValue::Number(n) => Ok(CriteriaPredicate::Eq(LiteralValue::Number(*n))),
164 LiteralValue::Int(i) => Ok(CriteriaPredicate::Eq(LiteralValue::Number(*i as f64))),
167 LiteralValue::Boolean(b) => Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(*b))),
168 LiteralValue::Error(e) => Err(e.clone()),
169 LiteralValue::Array(arr) => {
170 if arr.len() == 1 && arr.first().map(|r| r.len()).unwrap_or(0) == 1 {
172 parse_criteria(&arr[0][0])
173 } else {
174 Ok(CriteriaPredicate::Eq(LiteralValue::Array(arr.clone())))
175 }
176 }
177 other => Ok(CriteriaPredicate::Eq(other.clone())),
178 }
179}
180
181pub fn validate_and_prepare<'a, 'b>(
182 args: &'a [ArgumentHandle<'a, 'b>],
183 schema: &[ArgSchema],
184 options: ValidationOptions,
185) -> Result<PreparedArgs<'a>, ExcelError> {
186 if options.min_args > 0 && args.len() < options.min_args {
189 if options.warn_only {
190 return Ok(PreparedArgs { items: Vec::new() });
191 }
192 return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
193 "Too few arguments: expected at least {}, got {}",
194 options.min_args,
195 args.len()
196 )));
197 }
198
199 if schema.is_empty() {
201 return Ok(PreparedArgs { items: Vec::new() });
202 }
203
204 let mut items: Vec<PreparedArg<'a>> = Vec::with_capacity(args.len());
205 for (idx, arg) in args.iter().enumerate() {
206 let spec = if schema.len() == 1 {
207 &schema[0]
208 } else if idx < schema.len() {
209 &schema[idx]
210 } else {
211 if let Some(rep_spec) = schema.iter().find(|s| s.repeating.is_some()) {
213 rep_spec
214 } else if options.warn_only {
215 continue;
216 } else {
217 return Err(
218 ExcelError::new(ExcelErrorKind::Value).with_message("Too many arguments")
219 );
220 }
221 };
222
223 if spec.by_ref {
225 match arg.as_reference_or_eval() {
226 Ok(r) => {
227 items.push(PreparedArg::Reference(r));
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 if matches!(spec.coercion, CoercionPolicy::Criteria) {
242 let v = arg.value()?.into_literal();
243 match parse_criteria(&v) {
244 Ok(pred) => {
245 items.push(PreparedArg::Predicate(pred));
246 continue;
247 }
248 Err(e) => {
249 if options.warn_only {
250 continue;
251 } else {
252 return Err(e);
253 }
254 }
255 }
256 }
257
258 match spec.shape {
260 ShapeKind::Scalar => {
261 match arg.value() {
263 Ok(cv) => {
264 let v: Cow<'_, LiteralValue> = match cv {
265 crate::traits::CalcValue::Scalar(LiteralValue::Array(arr)) => {
266 let tl = arr
267 .first()
268 .and_then(|row| row.first())
269 .cloned()
270 .unwrap_or(LiteralValue::Empty);
271 Cow::Owned(tl)
272 }
273 crate::traits::CalcValue::Range(rv) => Cow::Owned(rv.get_cell(0, 0)),
274 crate::traits::CalcValue::Scalar(s) => Cow::Owned(s),
275 crate::traits::CalcValue::Callable(_) => {
276 Cow::Owned(LiteralValue::Error(
277 ExcelError::new(ExcelErrorKind::Calc)
278 .with_message("LAMBDA value must be invoked"),
279 ))
280 }
281 };
282 let coerced = match spec.coercion {
284 CoercionPolicy::None => v,
285 CoercionPolicy::NumberStrict => {
286 match crate::coercion::to_number_strict(v.as_ref()) {
287 Ok(n) => Cow::Owned(LiteralValue::Number(n)),
288 Err(e) => {
289 if options.warn_only {
290 v
291 } else {
292 return Err(e);
293 }
294 }
295 }
296 }
297 CoercionPolicy::NumberLenientText => {
298 match crate::coercion::to_number_lenient(v.as_ref()) {
299 Ok(n) => Cow::Owned(LiteralValue::Number(n)),
300 Err(e) => {
301 if options.warn_only {
302 v
303 } else {
304 return Err(e);
305 }
306 }
307 }
308 }
309 CoercionPolicy::Logical => {
310 match crate::coercion::to_logical(v.as_ref()) {
311 Ok(b) => Cow::Owned(LiteralValue::Boolean(b)),
312 Err(e) => {
313 if options.warn_only {
314 v
315 } else {
316 return Err(e);
317 }
318 }
319 }
320 }
321 CoercionPolicy::Criteria => v, CoercionPolicy::DateTimeSerial => {
323 match crate::coercion::to_datetime_serial(v.as_ref()) {
324 Ok(n) => Cow::Owned(LiteralValue::Number(n)),
325 Err(e) => {
326 if options.warn_only {
327 v
328 } else {
329 return Err(e);
330 }
331 }
332 }
333 }
334 };
335 items.push(PreparedArg::Value(coerced))
336 }
337 Err(e) => items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(e)))),
338 }
339 }
340 ShapeKind::Range | ShapeKind::Array => {
341 match arg.range_view() {
342 Ok(r) => items.push(PreparedArg::Range(r)),
343 Err(_e) => {
344 match arg.value() {
347 Ok(v) => items.push(PreparedArg::Value(Cow::Owned(v.into_literal()))),
348 Err(e2) => {
349 items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(e2))))
350 }
351 }
352 }
353 }
354 }
355 }
356 }
357
358 Ok(PreparedArgs { items })
359}