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