Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions base/src/expressions/parser/static_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,15 @@ fn args_signature_xlookup(arg_count: usize) -> Vec<Signature> {
result
}

fn args_signature_xmatch(arg_count: usize) -> Vec<Signature> {
if !(2..=4).contains(&arg_count) {
return vec![Signature::Error; arg_count];
}
let mut result = vec![Signature::Scalar; arg_count];
result[1] = Signature::Vector; // lookup_array should be Vector
result
}

fn args_signature_textafter(arg_count: usize) -> Vec<Signature> {
if !(2..=6).contains(&arg_count) {
vec![Signature::Scalar; arg_count]
Expand Down Expand Up @@ -657,6 +666,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Rows => args_signature_one_vector(arg_count),
Function::Vlookup => args_signature_hlookup(arg_count),
Function::Xlookup => args_signature_xlookup(arg_count),
Function::Xmatch => args_signature_xmatch(arg_count),
Function::Concat => vec![Signature::Vector; arg_count],
Function::Concatenate => vec![Signature::Scalar; arg_count],
Function::Exact => args_signature_scalars(arg_count, 2, 0),
Expand Down Expand Up @@ -862,6 +872,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Rows => not_implemented(args),
Function::Vlookup => not_implemented(args),
Function::Xlookup => not_implemented(args),
Function::Xmatch => not_implemented(args),
Function::Concat => not_implemented(args),
Function::Concatenate => not_implemented(args),
Function::Exact => not_implemented(args),
Expand Down
8 changes: 7 additions & 1 deletion base/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ pub enum Function {
Rows,
Vlookup,
Xlookup,
Xmatch,

// Text
Concat,
Expand Down Expand Up @@ -253,7 +254,7 @@ pub enum Function {
}

impl Function {
pub fn into_iter() -> IntoIter<Function, 198> {
pub fn into_iter() -> IntoIter<Function, 199> {
[
Function::And,
Function::False,
Expand Down Expand Up @@ -311,6 +312,7 @@ impl Function {
Function::Rows,
Function::Vlookup,
Function::Xlookup,
Function::Xmatch,
Function::Concatenate,
Function::Exact,
Function::Value,
Expand Down Expand Up @@ -469,6 +471,7 @@ impl Function {
Function::Minifs => "_xlfn.MINIFS".to_string(),
Function::Switch => "_xlfn.SWITCH".to_string(),
Function::Xlookup => "_xlfn.XLOOKUP".to_string(),
Function::Xmatch => "_xlfn.XMATCH".to_string(),
Function::Xor => "_xlfn.XOR".to_string(),
Function::Textbefore => "_xlfn.TEXTBEFORE".to_string(),
Function::Textafter => "_xlfn.TEXTAFTER".to_string(),
Expand Down Expand Up @@ -570,6 +573,7 @@ impl Function {
"ROWS" => Some(Function::Rows),
"VLOOKUP" => Some(Function::Vlookup),
"XLOOKUP" | "_XLFN.XLOOKUP" => Some(Function::Xlookup),
"XMATCH" | "_XLFN.XMATCH" => Some(Function::Xmatch),

"CONCATENATE" => Some(Function::Concatenate),
"EXACT" => Some(Function::Exact),
Expand Down Expand Up @@ -789,6 +793,7 @@ impl fmt::Display for Function {
Function::Rows => write!(f, "ROWS"),
Function::Vlookup => write!(f, "VLOOKUP"),
Function::Xlookup => write!(f, "XLOOKUP"),
Function::Xmatch => write!(f, "XMATCH"),
Function::Concatenate => write!(f, "CONCATENATE"),
Function::Exact => write!(f, "EXACT"),
Function::Value => write!(f, "VALUE"),
Expand Down Expand Up @@ -1027,6 +1032,7 @@ impl Model {
Function::Rows => self.fn_rows(args, cell),
Function::Vlookup => self.fn_vlookup(args, cell),
Function::Xlookup => self.fn_xlookup(args, cell),
Function::Xmatch => self.fn_xmatch(args, cell),
// Text
Function::Concatenate => self.fn_concatenate(args, cell),
Function::Exact => self.fn_exact(args, cell),
Expand Down
203 changes: 203 additions & 0 deletions base/src/functions/xlookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};

#[cfg(feature = "use_regex_lite")]
use regex_lite as regex;

use super::{
binary_search::{
binary_search_descending_or_greater, binary_search_descending_or_smaller,
Expand All @@ -26,6 +29,7 @@ enum MatchMode {
ExactMatch = 0,
ExactMatchLarger = 1,
WildcardMatch = 2,
RegexMatch = 3,
}

// lookup_value in array, match_mode search_mode
Expand Down Expand Up @@ -114,6 +118,29 @@ fn linear_search(
}
}
}
MatchMode::RegexMatch => {
let result_matches: Box<dyn Fn(&CalcResult) -> bool> =
if let CalcResult::String(s) = &lookup_value {
if let Ok(reg) = regex::Regex::new(&s.to_lowercase()) {
Box::new(move |x| result_matches_regex(x, &reg))
} 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
}
Expand Down Expand Up @@ -387,4 +414,180 @@ impl Model {
},
}
}

/// XMATCH(lookup_value, lookup_array, [match_mode], [search_mode])
/// Returns the relative position of an item in an array or range
pub(crate) fn fn_xmatch(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() < 2 || args.len() > 4 {
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;
}
let match_mode = if args.len() >= 3 {
match self.get_number(&args[2], cell) {
Ok(c) => match c.floor() as i32 {
-1 => MatchMode::ExactMatchSmaller,
0 => MatchMode::ExactMatch,
1 => MatchMode::ExactMatchLarger,
2 => MatchMode::WildcardMatch,
3 => MatchMode::RegexMatch,
_ => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Unexpected number".to_string(),
};
}
},
Err(s) => return s,
}
} else {
MatchMode::ExactMatch
};
let search_mode = if args.len() == 4 {
match self.get_number(&args[3], 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 {
SearchMode::StartAtFirstItem
};
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 {
return CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Second argument must be a vector".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) => CalcResult::Number(index as f64 + 1.0),
None => CalcResult::Error {
error: Error::NA,
origin: cell,
message: "Not found".to_string(),
},
}
}
SearchMode::BinarySearchAscending | SearchMode::BinarySearchDescending => {
let array = self.prepare_array(&left, &right, is_row_vector);
let index = match match_mode {
MatchMode::ExactMatchLarger => {
if search_mode == SearchMode::BinarySearchAscending {
binary_search_or_greater(&lookup_value, &array)
} else {
binary_search_descending_or_greater(&lookup_value, &array)
}
}
MatchMode::ExactMatchSmaller | MatchMode::ExactMatch => {
if search_mode == SearchMode::BinarySearchAscending {
binary_search_or_smaller(&lookup_value, &array)
} else {
binary_search_descending_or_smaller(&lookup_value, &array)
}
}
MatchMode::WildcardMatch | MatchMode::RegexMatch => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Cannot use wildcard in binary search".to_string(),
};
}
};
match index {
None => CalcResult::Error {
error: Error::NA,
origin: cell,
message: "Not found".to_string(),
},
Some(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 {
return CalcResult::Error {
error: Error::NA,
origin: cell,
message: "Not found".to_string(),
};
}
}
CalcResult::Number(l as f64 + 1.0)
}
}
}
}
}
error @ CalcResult::Error { .. } => error,
_ => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Range expected".to_string(),
},
}
}
}
1 change: 1 addition & 0 deletions base/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mod test_fn_sumifs;
mod test_fn_textbefore;
mod test_fn_textjoin;
mod test_fn_unicode;
mod test_fn_xmatch;
mod test_frozen_rows_columns;
mod test_general;
mod test_math;
Expand Down
Loading