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> {
128 match v {
129 LiteralValue::Text(s) => {
130 let s_trim = s.trim_start();
134
135 let unquote = |t: &str| -> String {
136 let q = t.trim();
137 if let Some(inner) = q.strip_prefix('"').and_then(|x| x.strip_suffix('"')) {
138 inner.replace("\"\"", "\"")
139 } else {
140 t.to_string()
141 }
142 };
143 let is_pattern = |t: &str| t.contains('*') || t.contains('?') || t.contains("~~");
144
145 let ops = [">=", "<=", "<>", ">", "<", "="];
147 for op in ops.iter() {
148 if let Some(rhs) = s_trim.strip_prefix(op) {
149 if let Some(n) = parse_criteria_number(rhs.trim())
152 .or_else(|| parse_criteria_datetime(rhs.trim()))
153 {
154 return Ok(match *op {
155 ">=" => CriteriaPredicate::Ge(n),
156 "<=" => CriteriaPredicate::Le(n),
157 ">" => CriteriaPredicate::Gt(n),
158 "<" => CriteriaPredicate::Lt(n),
159 "=" => CriteriaPredicate::Eq(LiteralValue::Number(n)),
160 "<>" => CriteriaPredicate::Ne(LiteralValue::Number(n)),
161 _ => unreachable!(),
162 });
163 }
164 let text = unquote(rhs);
166 let boolean = match text.to_ascii_lowercase().as_str() {
167 "true" => Some(true),
168 "false" => Some(false),
169 _ => None,
170 };
171 return Ok(match *op {
172 "=" if text.is_empty() => CriteriaPredicate::IsBlank,
173 "<>" if text.is_empty() => CriteriaPredicate::NotBlank,
174 "=" if is_pattern(&text) => CriteriaPredicate::TextLike {
175 pattern: text,
176 case_insensitive: true,
177 },
178 "<>" if is_pattern(&text) => CriteriaPredicate::NotTextLike {
179 pattern: text,
180 case_insensitive: true,
181 },
182 "=" => match boolean {
183 Some(b) => CriteriaPredicate::Eq(LiteralValue::Boolean(b)),
184 None => CriteriaPredicate::Eq(LiteralValue::Text(text)),
185 },
186 "<>" => match boolean {
187 Some(b) => CriteriaPredicate::Ne(LiteralValue::Boolean(b)),
188 None => CriteriaPredicate::Ne(LiteralValue::Text(text)),
189 },
190 ">=" => CriteriaPredicate::TextGe(text),
191 "<=" => CriteriaPredicate::TextLe(text),
192 ">" => CriteriaPredicate::TextGt(text),
193 "<" => CriteriaPredicate::TextLt(text),
194 _ => unreachable!(),
195 });
196 }
197 }
198
199 let plain = unquote(s);
200
201 if is_pattern(&plain) {
203 return Ok(CriteriaPredicate::TextLike {
204 pattern: plain,
205 case_insensitive: true,
206 });
207 }
208 let lower = plain.to_ascii_lowercase();
210 if lower == "true" {
211 return Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(true)));
212 } else if lower == "false" {
213 return Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(false)));
214 }
215 if parse_criteria_number(plain.trim()).is_none()
217 && let Some(serial) = parse_criteria_datetime(plain.trim())
218 {
219 return Ok(CriteriaPredicate::Eq(LiteralValue::Number(serial)));
220 }
221 Ok(CriteriaPredicate::Eq(LiteralValue::Text(plain)))
223 }
224 LiteralValue::Empty => Ok(CriteriaPredicate::Eq(LiteralValue::Number(0.0))),
226 LiteralValue::Number(n) => Ok(CriteriaPredicate::Eq(LiteralValue::Number(*n))),
227 LiteralValue::Int(i) => Ok(CriteriaPredicate::Eq(LiteralValue::Number(*i as f64))),
230 LiteralValue::Boolean(b) => Ok(CriteriaPredicate::Eq(LiteralValue::Boolean(*b))),
231 LiteralValue::Error(e) => Err(e.clone()),
232 LiteralValue::Array(arr) => {
233 if arr.len() == 1 && arr.first().map(|r| r.len()).unwrap_or(0) == 1 {
235 parse_criteria(&arr[0][0])
236 } else {
237 Ok(CriteriaPredicate::Eq(LiteralValue::Array(arr.clone())))
238 }
239 }
240 other => Ok(CriteriaPredicate::Eq(other.clone())),
241 }
242}
243
244fn parse_criteria_datetime(text: &str) -> Option<f64> {
248 if text.is_empty() || !text.bytes().any(|b| b.is_ascii_digit()) {
249 return None;
250 }
251 formualizer_common::parse_excel_datetime_text_to_serial_for(
252 formualizer_common::DateSystem::Excel1900,
253 text,
254 )
255}
256
257fn parse_criteria_number(text: &str) -> Option<f64> {
259 text.parse::<f64>().ok().filter(|n| n.is_finite())
260}
261
262pub fn validate_and_prepare<'a, 'b>(
263 args: &'a [ArgumentHandle<'a, 'b>],
264 schema: &[ArgSchema],
265 options: ValidationOptions,
266) -> Result<PreparedArgs<'a>, ExcelError> {
267 validate_and_prepare_inline(args, schema, options).map(|prepared| PreparedArgs {
268 items: prepared.items.into_vec(),
269 })
270}
271
272pub(crate) fn validate_and_prepare_inline<'a, 'b>(
275 args: &'a [ArgumentHandle<'a, 'b>],
276 schema: &[ArgSchema],
277 options: ValidationOptions,
278) -> Result<InlinePreparedArgs<'a>, ExcelError> {
279 if options.min_args > 0 && args.len() < options.min_args {
282 if options.warn_only {
283 return Ok(InlinePreparedArgs {
284 items: SmallVec::new(),
285 });
286 }
287 return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
288 "Too few arguments: expected at least {}, got {}",
289 options.min_args,
290 args.len()
291 )));
292 }
293
294 if schema.is_empty() {
296 return Ok(InlinePreparedArgs {
297 items: SmallVec::new(),
298 });
299 }
300
301 let mut items: SmallVec<[PreparedArg<'a>; 4]> = SmallVec::with_capacity(args.len());
302 for (idx, arg) in args.iter().enumerate() {
303 let spec = if schema.len() == 1 {
304 &schema[0]
305 } else if idx < schema.len() {
306 &schema[idx]
307 } else {
308 if let Some(rep_spec) = schema.iter().find(|s| s.repeating.is_some()) {
310 rep_spec
311 } else if options.warn_only {
312 continue;
313 } else {
314 return Err(
315 ExcelError::new(ExcelErrorKind::Value).with_message("Too many arguments")
316 );
317 }
318 };
319
320 if spec.by_ref {
322 match arg.as_reference_or_eval() {
323 Ok(r) => {
324 items.push(PreparedArg::Reference(r));
325 continue;
326 }
327 Err(e) => {
328 if options.warn_only {
329 continue;
330 } else {
331 return Err(e);
332 }
333 }
334 }
335 }
336
337 if matches!(spec.coercion, CoercionPolicy::Criteria) {
339 let v = arg.value()?.into_literal();
340 match parse_criteria(&v) {
341 Ok(pred) => {
342 items.push(PreparedArg::Predicate(pred));
343 continue;
344 }
345 Err(e) => {
346 if options.warn_only {
347 continue;
348 } else {
349 return Err(e);
350 }
351 }
352 }
353 }
354
355 match spec.shape {
357 ShapeKind::Scalar => {
358 match arg.value() {
360 Ok(cv) => {
361 let v: Cow<'_, LiteralValue> = match cv {
362 crate::traits::CalcValue::Scalar(LiteralValue::Array(arr)) => {
363 let tl = arr
364 .first()
365 .and_then(|row| row.first())
366 .cloned()
367 .unwrap_or(LiteralValue::Empty);
368 Cow::Owned(tl)
369 }
370 crate::traits::CalcValue::Range(rv) => Cow::Owned(rv.get_cell(0, 0)),
371 crate::traits::CalcValue::Scalar(s)
372 | crate::traits::CalcValue::AnnotatedScalar(s, _) => Cow::Owned(s),
373 crate::traits::CalcValue::Callable(_) => {
374 Cow::Owned(LiteralValue::Error(
375 ExcelError::new(ExcelErrorKind::Calc)
376 .with_message("LAMBDA value must be invoked"),
377 ))
378 }
379 };
380 let coerced = match spec.coercion {
382 CoercionPolicy::None => v,
383 CoercionPolicy::NumberStrict => {
384 match crate::coercion::to_number_strict(v.as_ref()) {
385 Ok(n) => Cow::Owned(LiteralValue::Number(n)),
386 Err(e) => {
387 if options.warn_only {
388 v
389 } else {
390 return Err(e);
391 }
392 }
393 }
394 }
395 CoercionPolicy::NumberLenientText => {
396 match crate::coercion::to_number_lenient(v.as_ref()) {
397 Ok(n) => Cow::Owned(LiteralValue::Number(n)),
398 Err(e) => {
399 if options.warn_only {
400 v
401 } else {
402 return Err(e);
403 }
404 }
405 }
406 }
407 CoercionPolicy::Logical => {
408 match crate::coercion::to_logical(v.as_ref()) {
409 Ok(b) => Cow::Owned(LiteralValue::Boolean(b)),
410 Err(e) => {
411 if options.warn_only {
412 v
413 } else {
414 return Err(e);
415 }
416 }
417 }
418 }
419 CoercionPolicy::Criteria => v, CoercionPolicy::DateTimeSerial => {
421 match crate::coercion::to_datetime_serial(v.as_ref()) {
422 Ok(n) => Cow::Owned(LiteralValue::Number(n)),
423 Err(e) => {
424 if options.warn_only {
425 v
426 } else {
427 return Err(e);
428 }
429 }
430 }
431 }
432 };
433 items.push(PreparedArg::Value(coerced))
434 }
435 Err(e) => items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(e)))),
436 }
437 }
438 ShapeKind::Range | ShapeKind::Array => match arg.resolve_once() {
439 Ok(crate::traits::ResolvedArgument::Range(range))
440 | Ok(crate::traits::ResolvedArgument::Value(crate::traits::CalcValue::Range(
441 range,
442 ))) => items.push(PreparedArg::Range(range)),
443 Ok(crate::traits::ResolvedArgument::Value(value)) => {
444 items.push(PreparedArg::Value(Cow::Owned(value.into_literal())))
446 }
447 Ok(crate::traits::ResolvedArgument::ReferenceError(error)) => {
448 items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(error))))
449 }
450 Err(error) => {
451 items.push(PreparedArg::Value(Cow::Owned(LiteralValue::Error(error))))
452 }
453 },
454 }
455 }
456
457 Ok(InlinePreparedArgs { items })
458}