use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::expressions::types::CellReferenceIndex; use crate::{ calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model, }; use super::{ binary_search::{ binary_search_descending_or_greater, binary_search_descending_or_smaller, binary_search_or_greater, binary_search_or_smaller, }, util::{compare_values, from_wildcard_to_regex, result_matches_regex}, }; #[derive(PartialEq)] enum SearchMode { StartAtFirstItem = 1, StartAtLastItem = -1, BinarySearchDescending = -2, BinarySearchAscending = 2, } #[derive(PartialEq)] enum MatchMode { ExactMatchSmaller = -1, ExactMatch = 0, ExactMatchLarger = 1, WildcardMatch = 2, } // lookup_value in array, match_mode search_mode fn linear_search( lookup_value: &CalcResult, array: &[CalcResult], search_mode: SearchMode, match_mode: MatchMode, ) -> Option { let length = array.len(); match match_mode { MatchMode::ExactMatch => { // exact match for l in 0..length { let index = if search_mode == SearchMode::StartAtFirstItem { l } else { length - l - 1 }; let value = &array[index]; if compare_values(value, lookup_value) == 0 { return Some(index); } } return None; } MatchMode::ExactMatchSmaller | MatchMode::ExactMatchLarger => { // exact match, if none found return the next smaller/larger item let mut found_index = 0; let mut approx = None; let m_mode = match_mode as i32; for l in 0..length { let index = if search_mode == SearchMode::StartAtFirstItem { l } else { length - l - 1 }; let value = &array[index]; let c = compare_values(value, lookup_value); if c == 0 { return Some(index); } else if c == m_mode { match approx { None => { approx = Some(value.clone()); found_index = index; } Some(ref p) => { if compare_values(p, value) == m_mode { approx = Some(value.clone()); found_index = index; } } } } } if approx.is_none() { return None; } else { return Some(found_index); } } MatchMode::WildcardMatch => { let result_matches: Box bool> = if let CalcResult::String(s) = &lookup_value { if let Ok(reg) = from_wildcard_to_regex(&s.to_lowercase(), true) { Box::new(move |x| result_matches_regex(x, ®)) } else { Box::new(move |_| false) } } else { Box::new(move |x| compare_values(x, lookup_value) == 0) }; for l in 0..length { let index = if search_mode == SearchMode::StartAtFirstItem { l } else { length - l - 1 }; let value = &array[index]; if result_matches(value) { return Some(index); } } } } None } impl Model { /// The XLOOKUP function searches a range or an array, and then returns the item corresponding /// to the first match it finds. If no match exists, then XLOOKUP can return the closest (approximate) match. /// =XLOOKUP(lookup_value, lookup_array, return_array, [if_not_found], [match_mode], [search_mode]) /// /// lookup_array and return_array must be column or row arrays and of the same dimension. /// Otherwise #VALUE! is returned /// [if_not_found] /// Where a valid match is not found, return the [if_not_found] text you supply. /// If a valid match is not found, and [if_not_found] is missing, #N/A is returned. /// /// [match_mode] /// Specify the match type: /// * 0 - Exact match. If none found, return #N/A. This is the default. /// * -1 - Exact match. If none found, return the next smaller item. /// * 1 - Exact match. If none found, return the next larger item. /// * 2 - A wildcard match where *, ?, and ~ have special meaning. /// /// [search_mode] /// Specify the search mode to use: /// * 1 - Perform a search starting at the first item. This is the default. /// * -1 - Perform a reverse search starting at the last item. /// * 2 - Perform a binary search that relies on lookup_array being sorted /// in ascending order. If not sorted, invalid results will be returned. /// * -2 - Perform a binary search that relies on lookup_array being sorted /// in descending order. If not sorted, invalid results will be returned. pub(crate) fn fn_xlookup(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { if args.len() < 3 || args.len() > 6 { return CalcResult::new_args_number_error(cell); } let lookup_value = self.evaluate_node_in_context(&args[0], cell); if lookup_value.is_error() { return lookup_value; } // Get optional arguments let if_not_found = if args.len() >= 4 { let v = self.evaluate_node_in_context(&args[3], cell); match v { CalcResult::EmptyArg => CalcResult::Error { error: Error::NA, origin: cell, message: "Not found".to_string(), }, _ => v, } } else { // default CalcResult::Error { error: Error::NA, origin: cell, message: "Not found".to_string(), } }; let match_mode = if args.len() >= 5 { match self.get_number(&args[4], cell) { Ok(c) => match c.floor() as i32 { -1 => MatchMode::ExactMatchSmaller, 1 => MatchMode::ExactMatchLarger, 0 => MatchMode::ExactMatch, 2 => MatchMode::WildcardMatch, _ => { return CalcResult::Error { error: Error::VALUE, origin: cell, message: "Unexpected number".to_string(), }; } }, Err(s) => return s, } } else { // default MatchMode::ExactMatch }; let search_mode = if args.len() == 6 { match self.get_number(&args[5], cell) { Ok(c) => match c.floor() as i32 { 1 => SearchMode::StartAtFirstItem, -1 => SearchMode::StartAtLastItem, -2 => SearchMode::BinarySearchDescending, 2 => SearchMode::BinarySearchAscending, _ => { return CalcResult::Error { error: Error::ERROR, origin: cell, message: "Unexpected number".to_string(), }; } }, Err(s) => return s, } } else { // default SearchMode::StartAtFirstItem }; // lookup_array match self.evaluate_node_in_context(&args[1], cell) { CalcResult::Range { left, right } => { let is_row_vector; if left.row == right.row { is_row_vector = false; } else if left.column == right.column { is_row_vector = true; } else { // second argument must be a vector return CalcResult::Error { error: Error::ERROR, origin: cell, message: "Second argument must be a vector".to_string(), }; } // return array match self.evaluate_node_in_context(&args[2], cell) { CalcResult::Range { left: result_left, right: result_right, } => { if result_right.row - result_left.row != right.row - left.row || result_right.column - result_left.column != right.column - left.column { return CalcResult::Error { error: Error::VALUE, origin: cell, message: "Arrays must be of the same size".to_string(), }; } let mut row2 = right.row; let row1 = left.row; let mut column2 = right.column; let column1 = left.column; if row1 == 1 && row2 == LAST_ROW { row2 = match self.workbook.worksheet(left.sheet) { Ok(s) => s.dimension().max_row, Err(_) => { return CalcResult::new_error( Error::ERROR, cell, format!("Invalid worksheet index: '{}'", left.sheet), ); } }; } if column1 == 1 && column2 == LAST_COLUMN { column2 = match self.workbook.worksheet(left.sheet) { Ok(s) => s.dimension().max_column, Err(_) => { return CalcResult::new_error( Error::ERROR, cell, format!("Invalid worksheet index: '{}'", left.sheet), ); } }; } let left = CellReferenceIndex { sheet: left.sheet, column: column1, row: row1, }; let right = CellReferenceIndex { sheet: left.sheet, column: column2, row: row2, }; match search_mode { SearchMode::StartAtFirstItem | SearchMode::StartAtLastItem => { let array = &self.prepare_array(&left, &right, is_row_vector); match linear_search(&lookup_value, array, search_mode, match_mode) { Some(index) => { let row_index = if is_row_vector { index as i32 } else { 0 }; let column_index = if is_row_vector { 0 } else { index as i32 }; self.evaluate_cell(CellReferenceIndex { sheet: result_left.sheet, row: result_left.row + row_index, column: result_left.column + column_index, }) } None => if_not_found, } } SearchMode::BinarySearchAscending | SearchMode::BinarySearchDescending => { let index = if match_mode == MatchMode::ExactMatchLarger { if search_mode == SearchMode::BinarySearchAscending { binary_search_or_greater( &lookup_value, &self.prepare_array(&left, &right, is_row_vector), ) } else { binary_search_descending_or_greater( &lookup_value, &self.prepare_array(&left, &right, is_row_vector), ) } } else if search_mode == SearchMode::BinarySearchAscending { binary_search_or_smaller( &lookup_value, &self.prepare_array(&left, &right, is_row_vector), ) } else { binary_search_descending_or_smaller( &lookup_value, &self.prepare_array(&left, &right, is_row_vector), ) }; match index { None => if_not_found, Some(l) => { let row = result_left.row + if is_row_vector { l } else { 0 }; let column = result_left.column + if is_row_vector { 0 } else { l }; if match_mode == MatchMode::ExactMatch { let value = self.evaluate_cell(CellReferenceIndex { sheet: left.sheet, row: left.row + if is_row_vector { l } else { 0 }, column: left.column + if is_row_vector { 0 } else { l }, }); if compare_values(&value, &lookup_value) == 0 { self.evaluate_cell(CellReferenceIndex { sheet: result_left.sheet, row, column, }) } else { if_not_found } } else if match_mode == MatchMode::ExactMatchSmaller || match_mode == MatchMode::ExactMatchLarger { self.evaluate_cell(CellReferenceIndex { sheet: result_left.sheet, row, column, }) } else { CalcResult::Error { error: Error::VALUE, origin: cell, message: "Cannot use wildcard in binary search" .to_string(), } } } } } } } error @ CalcResult::Error { .. } => error, _ => CalcResult::Error { error: Error::VALUE, origin: cell, message: "Range expected".to_string(), }, } } error @ CalcResult::Error { .. } => error, _ => CalcResult::Error { error: Error::NA, origin: cell, message: "Range expected".to_string(), }, } } }