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 NotTextLike {
71 pattern: String,
72 case_insensitive: bool,
73 },
74 TextGt(String),
77 TextGe(String),
78 TextLt(String),
79 TextLe(String),
80 IsBlank,
82 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 pub min_args: usize,
113}
114
115pub fn parse_criteria(v: &LiteralValue) -> Result<CriteriaPredicate, ExcelError> {
130 match v {
131 LiteralValue::Text(s) => {
132 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 let ops = [">=", "<=", "<>", ">", "<", "="];
149 for op in ops.iter() {
150 if let Some(rhs) = s_trim.strip_prefix(op) {
151 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 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 if is_pattern(&plain) {
215 return Ok(CriteriaPredicate::TextLike {
216 pattern: plain,
217 case_insensitive: true,
218 });
219 }
220 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 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 Ok(CriteriaPredicate::Eq(LiteralValue::Text(plain)))
235 }
236 LiteralValue::Empty => Ok(CriteriaPredicate::Eq(LiteralValue::Number(0.0))),
238 LiteralValue::Number(n) => Ok(CriteriaPredicate::Eq(LiteralValue::Number(*n))),
239 LiteralValue::Int(i) => Ok(CriteriaPredicate::Eq(LiteralValue::Number(*i as f64))),
242 LiteralValue::Boolean(b) => Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(*b))),
243 LiteralValue::Error(e) => Ok(CriteriaPredicate::Eq(LiteralValue::Error(e.clone()))),
245 LiteralValue::Array(arr) => {
246 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
257fn 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
266fn 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
279fn 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
294pub(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 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 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 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 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 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 match spec.shape {
379 ShapeKind::Scalar => {
380 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 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, 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 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}