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