1use super::lookup_utils::{cmp_for_lookup, find_exact_index, is_sorted_ascending};
13use crate::args::{ArgSchema, CoercionPolicy, ShapeKind};
14use crate::function::Function;
15use crate::traits::{ArgumentHandle, FunctionContext};
16use formualizer_common::ArgKind;
17use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
18use formualizer_macros::func_caps;
19use formualizer_parse::parser::ReferenceType;
20
21fn binary_search_match(slice: &[LiteralValue], needle: &LiteralValue, mode: i32) -> Option<usize> {
22 if mode == 0 || slice.is_empty() {
23 return None;
24 }
25 if mode == 1 {
27 let mut lo = 0usize;
29 let mut hi = slice.len();
30 while lo < hi {
31 let mid = (lo + hi) / 2;
32 match cmp_for_lookup(&slice[mid], needle) {
33 Some(c) => {
34 if c > 0 {
35 hi = mid;
36 } else {
37 lo = mid + 1;
38 }
39 }
40 None => {
41 hi = mid;
42 }
43 }
44 }
45 if lo == 0 { None } else { Some(lo - 1) }
46 } else {
47 let mut best: Option<usize> = None;
49 for (i, v) in slice.iter().enumerate() {
50 if let Some(c) = cmp_for_lookup(v, needle) {
51 if c == 0 {
52 return Some(i);
53 }
54 if c >= 0 && best.is_none_or(|b| i < b) {
55 best = Some(i);
56 }
57 }
58 }
59 best
60 }
61}
62
63#[derive(Debug)]
64pub struct MatchFn;
65impl Function for MatchFn {
66 fn name(&self) -> &'static str {
67 "MATCH"
68 }
69 fn min_args(&self) -> usize {
70 2
71 }
72 func_caps!(PURE, LOOKUP);
73 fn arg_schema(&self) -> &'static [ArgSchema] {
74 use once_cell::sync::Lazy;
75 static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
76 vec![
77 ArgSchema {
79 kinds: smallvec::smallvec![ArgKind::Any],
80 required: true,
81 by_ref: false,
82 shape: ShapeKind::Scalar,
83 coercion: CoercionPolicy::None,
84 max: None,
85 repeating: None,
86 default: None,
87 },
88 ArgSchema {
90 kinds: smallvec::smallvec![ArgKind::Range],
91 required: true,
92 by_ref: true,
93 shape: ShapeKind::Range,
94 coercion: CoercionPolicy::None,
95 max: None,
96 repeating: None,
97 default: None,
98 },
99 ArgSchema {
101 kinds: smallvec::smallvec![ArgKind::Number],
102 required: false,
103 by_ref: false,
104 shape: ShapeKind::Scalar,
105 coercion: CoercionPolicy::NumberLenientText,
106 max: None,
107 repeating: None,
108 default: Some(LiteralValue::Number(1.0)),
109 },
110 ]
111 });
112 &SCHEMA
113 }
114 fn eval_scalar<'a, 'b>(
115 &self,
116 args: &'a [ArgumentHandle<'a, 'b>],
117 ctx: &dyn FunctionContext,
118 ) -> Result<LiteralValue, ExcelError> {
119 if args.len() < 2 {
120 return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)));
121 }
122 let lookup_value = match args[0].value() {
123 Ok(v) => v,
124 Err(e) => return Ok(LiteralValue::Error(e)),
125 }; let mut match_type = 1.0; if args.len() >= 3 {
128 let mt_val = match args[2].value() {
129 Ok(v) => v,
130 Err(e) => return Ok(LiteralValue::Error(e)),
131 };
132 if let LiteralValue::Error(e) = mt_val.as_ref() {
133 return Ok(LiteralValue::Error(e.clone()));
134 }
135 match mt_val.as_ref() {
136 LiteralValue::Number(n) => match_type = *n,
137 LiteralValue::Int(i) => match_type = *i as f64,
138 LiteralValue::Text(s) => {
139 if let Ok(n) = s.parse::<f64>() {
140 match_type = n;
141 }
142 }
143 _ => {}
144 }
145 }
146 let mt = if match_type > 0.0 {
147 1
148 } else if match_type < 0.0 {
149 -1
150 } else {
151 0
152 };
153 let arr_ref = args[1].as_reference_or_eval().ok();
154 let mut values: Vec<LiteralValue> = Vec::new();
155 if let Some(r) = arr_ref {
156 let current_sheet = ctx.current_sheet();
157 match ctx.resolve_range_view(&r, current_sheet) {
158 Ok(rv) => {
159 if let Err(e) = rv.for_each_cell(&mut |v| {
160 values.push(v.clone());
161 Ok(())
162 }) {
163 return Ok(LiteralValue::Error(e));
164 }
165 }
166 Err(e) => return Ok(LiteralValue::Error(e)),
167 }
168 } else {
169 match args[1].value() {
170 Ok(v) => values.push(v.as_ref().clone()),
171 Err(e) => return Ok(LiteralValue::Error(e)),
172 }
173 }
174 if values.is_empty() {
175 return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)));
176 }
177 if mt == 0 {
178 let wildcard_mode = matches!(lookup_value.as_ref(), LiteralValue::Text(s) if s.contains('*') || s.contains('?') || s.contains('~'));
179 if let Some(idx) = find_exact_index(&values, lookup_value.as_ref(), wildcard_mode) {
180 return Ok(LiteralValue::Int((idx + 1) as i64));
181 }
182 return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)));
183 }
184 let is_sorted = if mt == 1 {
186 is_sorted_ascending(&values)
187 } else if mt == -1 {
188 values
189 .windows(2)
190 .all(|w| cmp_for_lookup(&w[0], &w[1]).is_some_and(|c| c >= 0))
191 } else {
192 true
193 };
194 if !is_sorted {
195 return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)));
196 }
197 let idx = if values.len() < 8 {
198 let mut best: Option<(usize, &LiteralValue)> = None;
200 for (i, v) in values.iter().enumerate() {
201 if let Some(c) = cmp_for_lookup(v, lookup_value.as_ref()) {
202 if mt == 1 {
204 if (c == 0 || c == -1) && (best.is_none() || i > best.unwrap().0) {
206 best = Some((i, v));
207 }
208 } else {
209 if (c == 0 || c == 1) && (best.is_none() || i > best.unwrap().0) {
211 best = Some((i, v));
212 }
213 }
214 }
215 }
216 best.map(|(i, _)| i)
217 } else {
218 binary_search_match(&values, lookup_value.as_ref(), mt)
219 };
220 match idx {
221 Some(i) => Ok(LiteralValue::Int((i + 1) as i64)),
222 None => Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na))),
223 }
224 }
225}
226
227#[derive(Debug)]
228pub struct VLookupFn;
229impl Function for VLookupFn {
230 fn name(&self) -> &'static str {
231 "VLOOKUP"
232 }
233 fn min_args(&self) -> usize {
234 3
235 }
236 func_caps!(PURE, LOOKUP);
237 fn arg_schema(&self) -> &'static [ArgSchema] {
238 use once_cell::sync::Lazy;
239 static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
240 vec![
241 ArgSchema {
243 kinds: smallvec::smallvec![ArgKind::Any],
244 required: true,
245 by_ref: false,
246 shape: ShapeKind::Scalar,
247 coercion: CoercionPolicy::None,
248 max: None,
249 repeating: None,
250 default: None,
251 },
252 ArgSchema {
254 kinds: smallvec::smallvec![ArgKind::Range],
255 required: true,
256 by_ref: true,
257 shape: ShapeKind::Range,
258 coercion: CoercionPolicy::None,
259 max: None,
260 repeating: None,
261 default: None,
262 },
263 ArgSchema {
265 kinds: smallvec::smallvec![ArgKind::Number],
266 required: true,
267 by_ref: false,
268 shape: ShapeKind::Scalar,
269 coercion: CoercionPolicy::NumberStrict,
270 max: None,
271 repeating: None,
272 default: None,
273 },
274 ArgSchema {
276 kinds: smallvec::smallvec![ArgKind::Logical],
277 required: false,
278 by_ref: false,
279 shape: ShapeKind::Scalar,
280 coercion: CoercionPolicy::Logical,
281 max: None,
282 repeating: None,
283 default: Some(LiteralValue::Boolean(false)),
284 },
285 ]
286 });
287 &SCHEMA
288 }
289 fn eval_scalar<'a, 'b>(
290 &self,
291 args: &'a [ArgumentHandle<'a, 'b>],
292 ctx: &dyn FunctionContext,
293 ) -> Result<LiteralValue, ExcelError> {
294 if args.len() < 3 {
295 return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)));
296 }
297 let lookup_value = match args[0].value() {
298 Ok(v) => v,
299 Err(e) => return Ok(LiteralValue::Error(e)),
300 };
301 let table_ref = match args[1].as_reference_or_eval() {
302 Ok(r) => r,
303 Err(e) => return Ok(LiteralValue::Error(e)),
304 };
305 let col_index = match args[2].value()?.as_ref() {
306 LiteralValue::Int(i) => *i,
307 LiteralValue::Number(n) => *n as i64,
308 _ => return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value))),
309 };
310 if col_index < 1 {
311 return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value)));
312 }
313 let approximate = if args.len() >= 4 {
314 match args[3].value()?.as_ref() {
315 LiteralValue::Boolean(b) => *b,
316 _ => true,
317 }
318 } else {
319 false };
321 let (sheet, sr, sc, er, ec) = match &table_ref {
322 ReferenceType::Range {
323 sheet,
324 start_row: Some(sr),
325 start_col: Some(sc),
326 end_row: Some(er),
327 end_col: Some(ec),
328 } => (sheet.clone(), *sr, *sc, *er, *ec),
329 _ => return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Ref))),
330 };
331 let current_sheet = ctx.current_sheet();
332 let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
333 let width = ec - sc + 1;
334 if col_index as u32 > width {
335 return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Ref)));
336 }
337 let mut first_col: Vec<LiteralValue> = Vec::new();
339 {
340 let rv = ctx.resolve_range_view(&table_ref, sheet_name)?;
341 let col_offset = 0usize;
342 rv.for_each_row(&mut |row| {
343 let v = row.get(col_offset).cloned().unwrap_or(LiteralValue::Empty);
344 first_col.push(v);
345 Ok(())
346 })?;
347 }
348 let row_idx_opt = if approximate {
349 if first_col.is_empty() {
350 None
351 } else {
352 binary_search_match(&first_col, lookup_value.as_ref(), 1)
353 }
354 } else {
355 let mut found = None;
356 for (i, v) in first_col.iter().enumerate() {
357 if let Some(c) = cmp_for_lookup(lookup_value.as_ref(), v)
358 && c == 0
359 {
360 found = Some(i);
361 break;
362 }
363 }
364 found
365 };
366 let row_idx = match row_idx_opt {
367 Some(i) => i,
368 None => return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na))),
369 };
370 let rv = ctx.resolve_range_view(&table_ref, sheet_name)?;
372 let mut current = 0usize;
373 let target_col_idx = ((sc + (col_index as u32) - 1) - sc) as usize; let mut out: Option<LiteralValue> = None;
375 rv.for_each_row(&mut |row| {
376 if current == row_idx {
377 out = Some(
378 row.get(target_col_idx)
379 .cloned()
380 .unwrap_or(LiteralValue::Empty),
381 );
382 }
383 current += 1;
384 Ok(())
385 })?;
386 Ok(out.unwrap_or(LiteralValue::Empty))
387 }
388}
389
390#[derive(Debug)]
391pub struct HLookupFn;
392impl Function for HLookupFn {
393 fn name(&self) -> &'static str {
394 "HLOOKUP"
395 }
396 fn min_args(&self) -> usize {
397 3
398 }
399 func_caps!(PURE, LOOKUP);
400 fn arg_schema(&self) -> &'static [ArgSchema] {
401 use once_cell::sync::Lazy;
402 static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
403 vec![
404 ArgSchema {
406 kinds: smallvec::smallvec![ArgKind::Any],
407 required: true,
408 by_ref: false,
409 shape: ShapeKind::Scalar,
410 coercion: CoercionPolicy::None,
411 max: None,
412 repeating: None,
413 default: None,
414 },
415 ArgSchema {
417 kinds: smallvec::smallvec![ArgKind::Range],
418 required: true,
419 by_ref: true,
420 shape: ShapeKind::Range,
421 coercion: CoercionPolicy::None,
422 max: None,
423 repeating: None,
424 default: None,
425 },
426 ArgSchema {
428 kinds: smallvec::smallvec![ArgKind::Number],
429 required: true,
430 by_ref: false,
431 shape: ShapeKind::Scalar,
432 coercion: CoercionPolicy::NumberStrict,
433 max: None,
434 repeating: None,
435 default: None,
436 },
437 ArgSchema {
439 kinds: smallvec::smallvec![ArgKind::Logical],
440 required: false,
441 by_ref: false,
442 shape: ShapeKind::Scalar,
443 coercion: CoercionPolicy::Logical,
444 max: None,
445 repeating: None,
446 default: Some(LiteralValue::Boolean(false)),
447 },
448 ]
449 });
450 &SCHEMA
451 }
452 fn eval_scalar<'a, 'b>(
453 &self,
454 args: &'a [ArgumentHandle<'a, 'b>],
455 ctx: &dyn FunctionContext,
456 ) -> Result<LiteralValue, ExcelError> {
457 if args.len() < 3 {
458 return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)));
459 }
460 let lookup_value = match args[0].value() {
461 Ok(v) => v,
462 Err(e) => return Ok(LiteralValue::Error(e)),
463 };
464 let table_ref = match args[1].as_reference_or_eval() {
465 Ok(r) => r,
466 Err(e) => return Ok(LiteralValue::Error(e)),
467 };
468 let row_index = match args[2].value()?.as_ref() {
469 LiteralValue::Int(i) => *i,
470 LiteralValue::Number(n) => *n as i64,
471 _ => return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value))),
472 };
473 if row_index < 1 {
474 return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value)));
475 }
476 let approximate = if args.len() >= 4 {
477 match args[3].value()?.as_ref() {
478 LiteralValue::Boolean(b) => *b,
479 _ => true,
480 }
481 } else {
482 false
483 };
484 let (sheet, sr, sc, er, ec) = match &table_ref {
485 ReferenceType::Range {
486 sheet,
487 start_row: Some(sr),
488 start_col: Some(sc),
489 end_row: Some(er),
490 end_col: Some(ec),
491 } => (sheet.clone(), *sr, *sc, *er, *ec),
492 _ => return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Ref))),
493 };
494 let current_sheet = ctx.current_sheet();
495 let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
496 let height = er - sr + 1;
497 if row_index as u32 > height {
498 return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Ref)));
499 }
500 let mut first_row: Vec<LiteralValue> = Vec::new();
501 {
502 let rv = ctx.resolve_range_view(&table_ref, sheet_name)?;
503 let mut row_counter = 0usize;
504 rv.for_each_row(&mut |row| {
505 if row_counter == 0 {
506 first_row.extend_from_slice(row);
507 }
508 row_counter += 1;
509 Ok(())
510 })?;
511 }
512 let col_idx_opt = if approximate {
513 binary_search_match(&first_row, lookup_value.as_ref(), 1)
514 } else {
515 let mut f = None;
516 for (i, v) in first_row.iter().enumerate() {
517 if let Some(c) = cmp_for_lookup(lookup_value.as_ref(), v)
518 && c == 0
519 {
520 f = Some(i);
521 break;
522 }
523 }
524 f
525 };
526 let col_idx = match col_idx_opt {
527 Some(i) => i,
528 None => return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na))),
529 };
530 let target_row_rel = (row_index as usize) - 1; let target_col_rel = col_idx; let rv = ctx.resolve_range_view(&table_ref, sheet_name)?;
533 let mut collected: Option<LiteralValue> = None;
534 let mut r_counter = 0usize;
535 rv.for_each_row(&mut |row| {
536 if r_counter == target_row_rel {
537 collected = Some(
538 row.get(target_col_rel)
539 .cloned()
540 .unwrap_or(LiteralValue::Empty),
541 );
542 }
543 r_counter += 1;
544 Ok(())
545 })?;
546 Ok(collected.unwrap_or(LiteralValue::Empty))
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use crate::test_workbook::TestWorkbook;
554 use crate::traits::ArgumentHandle;
555 use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
556 use std::sync::Arc;
557 fn lit(v: LiteralValue) -> ASTNode {
558 ASTNode::new(ASTNodeType::Literal(v), None)
559 }
560
561 #[test]
562 fn match_wildcard_and_descending_and_unsorted() {
563 let wb = TestWorkbook::new().with_function(Arc::new(MatchFn));
565 let wb = wb
566 .with_cell_a1("Sheet1", "A1", LiteralValue::Text("foo".into()))
567 .with_cell_a1("Sheet1", "A2", LiteralValue::Text("fob".into()))
568 .with_cell_a1("Sheet1", "A3", LiteralValue::Text("bar".into()))
569 .with_cell_a1("Sheet1", "A4", LiteralValue::Text("baz".into()));
570 let ctx = wb.interpreter();
571 let range = ASTNode::new(
572 ASTNodeType::Reference {
573 original: "A1:A4".into(),
574 reference: ReferenceType::Range {
575 sheet: None,
576 start_row: Some(1),
577 start_col: Some(1),
578 end_row: Some(4),
579 end_col: Some(1),
580 },
581 },
582 None,
583 );
584 let f = ctx.context.get_function("", "MATCH").unwrap();
585 let pat = lit(LiteralValue::Text("*o*".into()));
587 let zero = lit(LiteralValue::Int(0));
588 let args = vec![
589 ArgumentHandle::new(&pat, &ctx),
590 ArgumentHandle::new(&range, &ctx),
591 ArgumentHandle::new(&zero, &ctx),
592 ];
593 let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
594 assert_eq!(v, LiteralValue::Int(1));
595 let pat2 = lit(LiteralValue::Text("b?z".into()));
597 let args2 = vec![
598 ArgumentHandle::new(&pat2, &ctx),
599 ArgumentHandle::new(&range, &ctx),
600 ArgumentHandle::new(&zero, &ctx),
601 ];
602 let v2 = f.dispatch(&args2, &ctx.function_context(None)).unwrap();
603 assert_eq!(v2, LiteralValue::Int(4));
604 let pat3 = lit(LiteralValue::Text("z*".into()));
606 let args3 = vec![
607 ArgumentHandle::new(&pat3, &ctx),
608 ArgumentHandle::new(&range, &ctx),
609 ArgumentHandle::new(&zero, &ctx),
610 ];
611 let v3 = f.dispatch(&args3, &ctx.function_context(None)).unwrap();
612 assert!(matches!(v3, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Na));
613
614 let wb2 = TestWorkbook::new()
616 .with_function(Arc::new(MatchFn))
617 .with_cell_a1("Sheet1", "A1", LiteralValue::Int(50))
618 .with_cell_a1("Sheet1", "A2", LiteralValue::Int(40))
619 .with_cell_a1("Sheet1", "A3", LiteralValue::Int(30))
620 .with_cell_a1("Sheet1", "A4", LiteralValue::Int(20))
621 .with_cell_a1("Sheet1", "A5", LiteralValue::Int(10));
622 let ctx2 = wb2.interpreter();
623 let range2 = ASTNode::new(
624 ASTNodeType::Reference {
625 original: "A1:A5".into(),
626 reference: ReferenceType::Range {
627 sheet: None,
628 start_row: Some(1),
629 start_col: Some(1),
630 end_row: Some(5),
631 end_col: Some(1),
632 },
633 },
634 None,
635 );
636 let minus1 = lit(LiteralValue::Int(-1));
637 let thirty = lit(LiteralValue::Int(30));
638 let args_desc = vec![
639 ArgumentHandle::new(&thirty, &ctx2),
640 ArgumentHandle::new(&range2, &ctx2),
641 ArgumentHandle::new(&minus1, &ctx2),
642 ];
643 let v_desc = f
644 .dispatch(&args_desc, &ctx2.function_context(None))
645 .unwrap();
646 assert_eq!(v_desc, LiteralValue::Int(3));
647 let sixty = lit(LiteralValue::Int(60));
649 let args_desc2 = vec![
650 ArgumentHandle::new(&sixty, &ctx2),
651 ArgumentHandle::new(&range2, &ctx2),
652 ArgumentHandle::new(&minus1, &ctx2),
653 ];
654 let v_desc2 = f
655 .dispatch(&args_desc2, &ctx2.function_context(None))
656 .unwrap();
657 assert!(matches!(v_desc2, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Na));
658
659 let wb3 = TestWorkbook::new()
661 .with_function(Arc::new(MatchFn))
662 .with_cell_a1("Sheet1", "A1", LiteralValue::Int(10))
663 .with_cell_a1("Sheet1", "A2", LiteralValue::Int(30))
664 .with_cell_a1("Sheet1", "A3", LiteralValue::Int(20))
665 .with_cell_a1("Sheet1", "A4", LiteralValue::Int(40))
666 .with_cell_a1("Sheet1", "A5", LiteralValue::Int(50));
667 let ctx3 = wb3.interpreter();
668 let range3 = ASTNode::new(
669 ASTNodeType::Reference {
670 original: "A1:A5".into(),
671 reference: ReferenceType::Range {
672 sheet: None,
673 start_row: Some(1),
674 start_col: Some(1),
675 end_row: Some(5),
676 end_col: Some(1),
677 },
678 },
679 None,
680 );
681 let args_unsorted = vec![
682 ArgumentHandle::new(&thirty, &ctx3),
683 ArgumentHandle::new(&range3, &ctx3),
684 ];
685 let v_unsorted = f
686 .dispatch(&args_unsorted, &ctx3.function_context(None))
687 .unwrap();
688 assert!(matches!(v_unsorted, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Na));
689 let wb4 = TestWorkbook::new()
691 .with_function(Arc::new(MatchFn))
692 .with_cell_a1("Sheet1", "A1", LiteralValue::Int(50))
693 .with_cell_a1("Sheet1", "A2", LiteralValue::Int(30))
694 .with_cell_a1("Sheet1", "A3", LiteralValue::Int(40))
695 .with_cell_a1("Sheet1", "A4", LiteralValue::Int(20))
696 .with_cell_a1("Sheet1", "A5", LiteralValue::Int(10));
697 let ctx4 = wb4.interpreter();
698 let range4 = ASTNode::new(
699 ASTNodeType::Reference {
700 original: "A1:A5".into(),
701 reference: ReferenceType::Range {
702 sheet: None,
703 start_row: Some(1),
704 start_col: Some(1),
705 end_row: Some(5),
706 end_col: Some(1),
707 },
708 },
709 None,
710 );
711 let args_unsorted_desc = vec![
712 ArgumentHandle::new(&thirty, &ctx4),
713 ArgumentHandle::new(&range4, &ctx4),
714 ArgumentHandle::new(&minus1, &ctx4),
715 ];
716 let v_unsorted_desc = f
717 .dispatch(&args_unsorted_desc, &ctx4.function_context(None))
718 .unwrap();
719 assert!(matches!(v_unsorted_desc, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Na));
720 }
721
722 #[test]
723 fn match_exact_and_approx() {
724 let wb = TestWorkbook::new().with_function(Arc::new(MatchFn));
725 let wb = wb
726 .with_cell_a1("Sheet1", "A1", LiteralValue::Int(10))
727 .with_cell_a1("Sheet1", "A2", LiteralValue::Int(20))
728 .with_cell_a1("Sheet1", "A3", LiteralValue::Int(30))
729 .with_cell_a1("Sheet1", "A4", LiteralValue::Int(40))
730 .with_cell_a1("Sheet1", "A5", LiteralValue::Int(50));
731 let ctx = wb.interpreter();
732 let range = ASTNode::new(
733 ASTNodeType::Reference {
734 original: "A1:A5".into(),
735 reference: ReferenceType::Range {
736 sheet: None,
737 start_row: Some(1),
738 start_col: Some(1),
739 end_row: Some(5),
740 end_col: Some(1),
741 },
742 },
743 None,
744 );
745 let f = ctx.context.get_function("", "MATCH").unwrap();
746 let thirty = lit(LiteralValue::Int(30));
747 let zero = lit(LiteralValue::Int(0));
748 let args = vec![
749 ArgumentHandle::new(&thirty, &ctx),
750 ArgumentHandle::new(&range, &ctx),
751 ArgumentHandle::new(&zero, &ctx),
752 ];
753 let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
754 assert_eq!(v, LiteralValue::Int(3));
755 let thirty_seven = lit(LiteralValue::Int(37));
756 let args = vec![
757 ArgumentHandle::new(&thirty_seven, &ctx),
758 ArgumentHandle::new(&range, &ctx),
759 ];
760 let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
761 assert_eq!(v, LiteralValue::Int(3));
762 }
763
764 #[test]
765 fn match_lookup_value_error_propagates() {
766 let wb = TestWorkbook::new()
767 .with_function(Arc::new(MatchFn))
768 .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1));
769 let ctx = wb.interpreter();
770 let range = ASTNode::new(
771 ASTNodeType::Reference {
772 original: "A1".into(),
773 reference: ReferenceType::Range {
774 sheet: None,
775 start_row: Some(1),
776 start_col: Some(1),
777 end_row: Some(1),
778 end_col: Some(1),
779 },
780 },
781 None,
782 );
783 let f = ctx.context.get_function("", "MATCH").unwrap();
784 let err_lookup = lit(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Div)));
785 let zero = lit(LiteralValue::Int(0));
786 let args = vec![
787 ArgumentHandle::new(&err_lookup, &ctx),
788 ArgumentHandle::new(&range, &ctx),
789 ArgumentHandle::new(&zero, &ctx),
790 ];
791 let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
792 assert!(matches!(v, LiteralValue::Error(_)));
795 }
796
797 #[test]
798 fn vlookup_negative_and_approximate() {
799 let wb = TestWorkbook::new()
800 .with_function(Arc::new(VLookupFn))
801 .with_cell_a1("Sheet1", "A1", LiteralValue::Int(10))
802 .with_cell_a1("Sheet1", "A2", LiteralValue::Int(20))
803 .with_cell_a1("Sheet1", "A3", LiteralValue::Int(30))
804 .with_cell_a1("Sheet1", "B1", LiteralValue::Text("Ten".into()))
805 .with_cell_a1("Sheet1", "B2", LiteralValue::Text("Twenty".into()))
806 .with_cell_a1("Sheet1", "B3", LiteralValue::Text("Thirty".into()));
807 let ctx = wb.interpreter();
808 let table = ASTNode::new(
809 ASTNodeType::Reference {
810 original: "A1:B3".into(),
811 reference: ReferenceType::Range {
812 sheet: None,
813 start_row: Some(1),
814 start_col: Some(1),
815 end_row: Some(3),
816 end_col: Some(2),
817 },
818 },
819 None,
820 );
821 let f = ctx.context.get_function("", "VLOOKUP").unwrap();
822 let fifteen = lit(LiteralValue::Int(15));
824 let neg_one = lit(LiteralValue::Int(-1));
825 let true_lit = lit(LiteralValue::Boolean(true));
826 let args_neg = vec![
827 ArgumentHandle::new(&fifteen, &ctx),
828 ArgumentHandle::new(&table, &ctx),
829 ArgumentHandle::new(&neg_one, &ctx),
830 ArgumentHandle::new(&true_lit, &ctx),
831 ];
832 let v_neg = f.dispatch(&args_neg, &ctx.function_context(None)).unwrap();
833 assert!(matches!(v_neg, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Value));
834 let two = lit(LiteralValue::Int(2));
836 let args_approx = vec![
837 ArgumentHandle::new(&fifteen, &ctx),
838 ArgumentHandle::new(&table, &ctx),
839 ArgumentHandle::new(&two, &ctx),
840 ArgumentHandle::new(&true_lit, &ctx),
841 ];
842 let v_approx = f
843 .dispatch(&args_approx, &ctx.function_context(None))
844 .unwrap();
845 assert_eq!(v_approx, LiteralValue::Text("Ten".into()));
846 }
847
848 #[test]
849 fn hlookup_negative_and_approximate() {
850 let wb = TestWorkbook::new()
851 .with_function(Arc::new(HLookupFn))
852 .with_cell_a1("Sheet1", "A1", LiteralValue::Int(10))
853 .with_cell_a1("Sheet1", "B1", LiteralValue::Int(20))
854 .with_cell_a1("Sheet1", "C1", LiteralValue::Int(30))
855 .with_cell_a1("Sheet1", "A2", LiteralValue::Text("Ten".into()))
856 .with_cell_a1("Sheet1", "B2", LiteralValue::Text("Twenty".into()))
857 .with_cell_a1("Sheet1", "C2", LiteralValue::Text("Thirty".into()));
858 let ctx = wb.interpreter();
859 let table = ASTNode::new(
860 ASTNodeType::Reference {
861 original: "A1:C2".into(),
862 reference: ReferenceType::Range {
863 sheet: None,
864 start_row: Some(1),
865 start_col: Some(1),
866 end_row: Some(2),
867 end_col: Some(3),
868 },
869 },
870 None,
871 );
872 let f = ctx.context.get_function("", "HLOOKUP").unwrap();
873 let fifteen = lit(LiteralValue::Int(15));
874 let neg_one = lit(LiteralValue::Int(-1));
875 let true_lit = lit(LiteralValue::Boolean(true));
876 let args_neg = vec![
877 ArgumentHandle::new(&fifteen, &ctx),
878 ArgumentHandle::new(&table, &ctx),
879 ArgumentHandle::new(&neg_one, &ctx),
880 ArgumentHandle::new(&true_lit, &ctx),
881 ];
882 let v_neg = f.dispatch(&args_neg, &ctx.function_context(None)).unwrap();
883 assert!(matches!(v_neg, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Value));
884 let two = lit(LiteralValue::Int(2));
886 let args_approx = vec![
887 ArgumentHandle::new(&fifteen, &ctx),
888 ArgumentHandle::new(&table, &ctx),
889 ArgumentHandle::new(&two, &ctx),
890 ArgumentHandle::new(&true_lit, &ctx),
891 ];
892 let v_approx = f
893 .dispatch(&args_approx, &ctx.function_context(None))
894 .unwrap();
895 assert_eq!(v_approx, LiteralValue::Text("Ten".into()));
896 }
897
898 #[test]
899 fn vlookup_basic() {
900 let wb = TestWorkbook::new()
901 .with_function(Arc::new(VLookupFn))
902 .with_cell_a1("Sheet1", "A1", LiteralValue::Text("Key1".into()))
903 .with_cell_a1("Sheet1", "A2", LiteralValue::Text("Key2".into()))
904 .with_cell_a1("Sheet1", "B1", LiteralValue::Int(100))
905 .with_cell_a1("Sheet1", "B2", LiteralValue::Int(200));
906 let ctx = wb.interpreter();
907 let table = ASTNode::new(
908 ASTNodeType::Reference {
909 original: "A1:B2".into(),
910 reference: ReferenceType::Range {
911 sheet: None,
912 start_row: Some(1),
913 start_col: Some(1),
914 end_row: Some(2),
915 end_col: Some(2),
916 },
917 },
918 None,
919 );
920 let f = ctx.context.get_function("", "VLOOKUP").unwrap();
921 let key2 = lit(LiteralValue::Text("Key2".into()));
922 let two = lit(LiteralValue::Int(2));
923 let false_lit = lit(LiteralValue::Boolean(false));
924 let args = vec![
925 ArgumentHandle::new(&key2, &ctx),
926 ArgumentHandle::new(&table, &ctx),
927 ArgumentHandle::new(&two, &ctx),
928 ArgumentHandle::new(&false_lit, &ctx),
929 ];
930 let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
931 assert_eq!(v, LiteralValue::Int(200));
932 }
933
934 #[test]
935 fn vlookup_default_exact_behavior() {
936 let wb = TestWorkbook::new()
937 .with_function(Arc::new(VLookupFn))
938 .with_cell_a1("Sheet1", "A1", LiteralValue::Int(10))
939 .with_cell_a1("Sheet1", "A2", LiteralValue::Int(20))
940 .with_cell_a1("Sheet1", "B1", LiteralValue::Text("Ten".into()))
941 .with_cell_a1("Sheet1", "B2", LiteralValue::Text("Twenty".into()));
942 let ctx = wb.interpreter();
943 let table = ASTNode::new(
944 ASTNodeType::Reference {
945 original: "A1:B2".into(),
946 reference: ReferenceType::Range {
947 sheet: None,
948 start_row: Some(1),
949 start_col: Some(1),
950 end_row: Some(2),
951 end_col: Some(2),
952 },
953 },
954 None,
955 );
956 let f = ctx.context.get_function("", "VLOOKUP").unwrap();
957 let fifteen = lit(LiteralValue::Int(15));
959 let two = lit(LiteralValue::Int(2));
960 let args = vec![
961 ArgumentHandle::new(&fifteen, &ctx),
962 ArgumentHandle::new(&table, &ctx),
963 ArgumentHandle::new(&two, &ctx),
964 ];
965 let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
966 assert!(matches!(v, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Na));
967 let twenty = lit(LiteralValue::Int(20));
969 let args2 = vec![
970 ArgumentHandle::new(&twenty, &ctx),
971 ArgumentHandle::new(&table, &ctx),
972 ArgumentHandle::new(&two, &ctx),
973 ];
974 let v2 = f.dispatch(&args2, &ctx.function_context(None)).unwrap();
975 assert_eq!(v2, LiteralValue::Text("Twenty".into()));
976 }
977
978 #[test]
979 fn hlookup_basic() {
980 let wb = TestWorkbook::new()
981 .with_function(Arc::new(HLookupFn))
982 .with_cell_a1("Sheet1", "A1", LiteralValue::Text("Key1".into()))
983 .with_cell_a1("Sheet1", "B1", LiteralValue::Text("Key2".into()))
984 .with_cell_a1("Sheet1", "A2", LiteralValue::Int(100))
985 .with_cell_a1("Sheet1", "B2", LiteralValue::Int(200));
986 let ctx = wb.interpreter();
987 let table = ASTNode::new(
988 ASTNodeType::Reference {
989 original: "A1:B2".into(),
990 reference: ReferenceType::Range {
991 sheet: None,
992 start_row: Some(1),
993 start_col: Some(1),
994 end_row: Some(2),
995 end_col: Some(2),
996 },
997 },
998 None,
999 );
1000 let f = ctx.context.get_function("", "HLOOKUP").unwrap();
1001 let key1 = lit(LiteralValue::Text("Key1".into()));
1002 let two = lit(LiteralValue::Int(2));
1003 let false_lit = lit(LiteralValue::Boolean(false));
1004 let args = vec![
1005 ArgumentHandle::new(&key1, &ctx),
1006 ArgumentHandle::new(&table, &ctx),
1007 ArgumentHandle::new(&two, &ctx),
1008 ArgumentHandle::new(&false_lit, &ctx),
1009 ];
1010 let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
1011 assert_eq!(v, LiteralValue::Int(100));
1012 }
1013
1014 #[test]
1015 fn hlookup_default_exact_behavior() {
1016 let wb = TestWorkbook::new()
1017 .with_function(Arc::new(HLookupFn))
1018 .with_cell_a1("Sheet1", "A1", LiteralValue::Int(10))
1019 .with_cell_a1("Sheet1", "B1", LiteralValue::Int(20))
1020 .with_cell_a1("Sheet1", "A2", LiteralValue::Text("Ten".into()))
1021 .with_cell_a1("Sheet1", "B2", LiteralValue::Text("Twenty".into()));
1022 let ctx = wb.interpreter();
1023 let table = ASTNode::new(
1024 ASTNodeType::Reference {
1025 original: "A1:B2".into(),
1026 reference: ReferenceType::Range {
1027 sheet: None,
1028 start_row: Some(1),
1029 start_col: Some(1),
1030 end_row: Some(2),
1031 end_col: Some(2),
1032 },
1033 },
1034 None,
1035 );
1036 let f = ctx.context.get_function("", "HLOOKUP").unwrap();
1037 let fifteen = lit(LiteralValue::Int(15));
1039 let two = lit(LiteralValue::Int(2));
1040 let args = vec![
1041 ArgumentHandle::new(&fifteen, &ctx),
1042 ArgumentHandle::new(&table, &ctx),
1043 ArgumentHandle::new(&two, &ctx),
1044 ];
1045 let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
1046 assert!(matches!(v, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Na));
1047 let twenty = lit(LiteralValue::Int(20));
1049 let args2 = vec![
1050 ArgumentHandle::new(&twenty, &ctx),
1051 ArgumentHandle::new(&table, &ctx),
1052 ArgumentHandle::new(&two, &ctx),
1053 ];
1054 let v2 = f.dispatch(&args2, &ctx.function_context(None)).unwrap();
1055 assert_eq!(v2, LiteralValue::Text("Twenty".into()));
1056 }
1057
1058 #[test]
1061 fn match_not_found_exact_and_approx_low() {
1062 let wb = TestWorkbook::new()
1063 .with_function(Arc::new(MatchFn))
1064 .with_cell_a1("Sheet1", "A1", LiteralValue::Int(10))
1065 .with_cell_a1("Sheet1", "A2", LiteralValue::Int(20))
1066 .with_cell_a1("Sheet1", "A3", LiteralValue::Int(30));
1067 let ctx = wb.interpreter();
1068 let range = ASTNode::new(
1069 ASTNodeType::Reference {
1070 original: "A1:A3".into(),
1071 reference: ReferenceType::Range {
1072 sheet: None,
1073 start_row: Some(1),
1074 start_col: Some(1),
1075 end_row: Some(3),
1076 end_col: Some(1),
1077 },
1078 },
1079 None,
1080 );
1081 let f = ctx.context.get_function("", "MATCH").unwrap();
1082 let needle_exact = lit(LiteralValue::Int(25));
1084 let zero = lit(LiteralValue::Int(0));
1085 let args_exact = vec![
1086 ArgumentHandle::new(&needle_exact, &ctx),
1087 ArgumentHandle::new(&range, &ctx),
1088 ArgumentHandle::new(&zero, &ctx),
1089 ];
1090 let v_exact = f
1091 .dispatch(&args_exact, &ctx.function_context(None))
1092 .unwrap();
1093 assert!(matches!(v_exact, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Na));
1094 let five = lit(LiteralValue::Int(5));
1096 let args_low = vec![
1097 ArgumentHandle::new(&five, &ctx),
1098 ArgumentHandle::new(&range, &ctx),
1099 ];
1100 let v_low = f.dispatch(&args_low, &ctx.function_context(None)).unwrap();
1101 assert!(matches!(v_low, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Na));
1102 }
1103
1104 #[test]
1105 fn vlookup_col_index_out_of_range_and_exact_not_found() {
1106 let wb = TestWorkbook::new()
1107 .with_function(Arc::new(VLookupFn))
1108 .with_cell_a1("Sheet1", "A1", LiteralValue::Text("A".into()))
1109 .with_cell_a1("Sheet1", "A2", LiteralValue::Text("B".into()))
1110 .with_cell_a1("Sheet1", "B1", LiteralValue::Int(1))
1111 .with_cell_a1("Sheet1", "B2", LiteralValue::Int(2));
1112 let ctx = wb.interpreter();
1113 let table = ASTNode::new(
1114 ASTNodeType::Reference {
1115 original: "A1:B2".into(),
1116 reference: ReferenceType::Range {
1117 sheet: None,
1118 start_row: Some(1),
1119 start_col: Some(1),
1120 end_row: Some(2),
1121 end_col: Some(2),
1122 },
1123 },
1124 None,
1125 );
1126 let f = ctx.context.get_function("", "VLOOKUP").unwrap();
1127 let key_a = lit(LiteralValue::Text("A".into()));
1129 let three = lit(LiteralValue::Int(3));
1130 let false_lit = lit(LiteralValue::Boolean(false));
1131 let args_bad_col = vec![
1132 ArgumentHandle::new(&key_a, &ctx),
1133 ArgumentHandle::new(&table, &ctx),
1134 ArgumentHandle::new(&three, &ctx),
1135 ArgumentHandle::new(&false_lit, &ctx),
1136 ];
1137 let v_bad_col = f
1138 .dispatch(&args_bad_col, &ctx.function_context(None))
1139 .unwrap();
1140 assert!(matches!(v_bad_col, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Ref));
1141 let key_missing = lit(LiteralValue::Text("Z".into()));
1143 let two = lit(LiteralValue::Int(2));
1144 let args_not_found = vec![
1145 ArgumentHandle::new(&key_missing, &ctx),
1146 ArgumentHandle::new(&table, &ctx),
1147 ArgumentHandle::new(&two, &ctx),
1148 ArgumentHandle::new(&false_lit, &ctx),
1149 ];
1150 let v_nf = f
1151 .dispatch(&args_not_found, &ctx.function_context(None))
1152 .unwrap();
1153 assert!(matches!(v_nf, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Na));
1154 }
1155
1156 #[test]
1157 fn hlookup_row_index_zero_and_arg_schema_type_error() {
1158 let wb = TestWorkbook::new()
1159 .with_function(Arc::new(HLookupFn))
1160 .with_cell_a1("Sheet1", "A1", LiteralValue::Text("A".into()))
1161 .with_cell_a1("Sheet1", "B1", LiteralValue::Text("B".into()))
1162 .with_cell_a1("Sheet1", "A2", LiteralValue::Int(1))
1163 .with_cell_a1("Sheet1", "B2", LiteralValue::Int(2));
1164 let ctx = wb.interpreter();
1165 let table = ASTNode::new(
1166 ASTNodeType::Reference {
1167 original: "A1:B2".into(),
1168 reference: ReferenceType::Range {
1169 sheet: None,
1170 start_row: Some(1),
1171 start_col: Some(1),
1172 end_row: Some(2),
1173 end_col: Some(2),
1174 },
1175 },
1176 None,
1177 );
1178 let f = ctx.context.get_function("", "HLOOKUP").unwrap();
1179 let key_a = lit(LiteralValue::Text("A".into()));
1181 let zero = lit(LiteralValue::Int(0));
1182 let false_lit = lit(LiteralValue::Boolean(false));
1183 let args_zero = vec![
1184 ArgumentHandle::new(&key_a, &ctx),
1185 ArgumentHandle::new(&table, &ctx),
1186 ArgumentHandle::new(&zero, &ctx),
1187 ArgumentHandle::new(&false_lit, &ctx),
1188 ];
1189 let v_zero = f.dispatch(&args_zero, &ctx.function_context(None)).unwrap();
1190 assert!(matches!(v_zero, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Value));
1191 let true_lit = lit(LiteralValue::Boolean(true));
1193 let args_type = vec![
1194 ArgumentHandle::new(&key_a, &ctx),
1195 ArgumentHandle::new(&table, &ctx),
1196 ArgumentHandle::new(&true_lit, &ctx),
1197 ];
1198 let v_type = f.dispatch(&args_type, &ctx.function_context(None)).unwrap();
1199 assert!(matches!(v_type, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Value));
1200 }
1201
1202 #[test]
1203 fn match_invalid_second_arg_not_range_triggers_ref_error() {
1204 let wb = TestWorkbook::new().with_function(Arc::new(MatchFn));
1205 let ctx = wb.interpreter();
1206 let f = ctx.context.get_function("", "MATCH").unwrap();
1207 let scalar_lookup = lit(LiteralValue::Int(10));
1208 let scalar_array = lit(LiteralValue::Int(20)); let args = vec![
1210 ArgumentHandle::new(&scalar_lookup, &ctx),
1211 ArgumentHandle::new(&scalar_array, &ctx),
1212 ];
1213 let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
1214 assert!(matches!(v, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Ref));
1215 }
1216}