Skip to content
Merged
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
29 changes: 29 additions & 0 deletions src/calculate/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,35 @@ where
(first, middle, last)
}

/// Returns (find_text, within_text, start_num) for FIND/SEARCH.
/// start_num defaults to 1 if only two arguments are provided.
pub fn get_find_args<N>(
mut exp: types::Expression<N>,
f: Option<&impl Fn(String) -> types::Value<N>>,
) -> (types::Value<N>, types::Value<N>, types::Value<N>)
where
N: XlNum,
<N as FromStr>::Err: Debug,
{
let start_num = if exp.values.len() >= 3 {
match exp.values.pop() {
Some(formula) => calculate_formula(formula, f),
None => types::Value::Number(N::one()),
}
} else {
types::Value::Number(N::one())
};
let within_text = match exp.values.pop() {
Some(formula) => calculate_formula(formula, f),
None => types::Value::Error(types::Error::Argument),
};
let find_text = match exp.values.pop() {
Some(formula) => calculate_formula(formula, f),
None => types::Value::Error(types::Error::Argument),
};
(find_text, within_text, start_num)
}

pub fn get_number_and_string_values<N>(
mut exp: types::Expression<N>,
f: Option<&impl Fn(String) -> types::Value<N>>,
Expand Down
108 changes: 105 additions & 3 deletions src/calculate/operation/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ use super::{
calculate_abs, calculate_average, calculate_collective_operator,
calculate_collective_product_operator,
},
string::{
find_position_case_sensitive, search_position_with_wildcards, value_to_string_for_find,
CoerceForFind,
},
};
use crate::{
calculate::args::{
get_binary_function_args, get_number_and_string_values, get_ternary_function_args,
get_unary_function_arg,
get_binary_function_args, get_find_args, get_number_and_string_values,
get_ternary_function_args, get_unary_function_arg,
},
types::{self, XlNum},
types::{self, Error, XlNum},
};
use std::{fmt::Debug, str::FromStr};

Expand Down Expand Up @@ -135,5 +139,103 @@ where
types::Function::Year => calculate_year(get_unary_function_arg(exp, f)),
types::Function::Month => calculate_month(get_unary_function_arg(exp, f)),
types::Function::Day => calculate_day(get_unary_function_arg(exp, f)),
types::Function::Find => calculate_find(get_find_args(exp, f)),
types::Function::Search => calculate_search(get_find_args(exp, f)),
types::Function::IsError => calculate_iserror(get_unary_function_arg(exp, f)),
}
}

fn calculate_find<N>(
(find_text, within_text, start_num): (types::Value<N>, types::Value<N>, types::Value<N>),
) -> types::Value<N>
where
N: XlNum,
<N as FromStr>::Err: Debug,
{
if let types::Value::Error(e) = find_text {
return types::Value::Error(e);
}
if let types::Value::Error(e) = within_text {
return types::Value::Error(e);
}
if let types::Value::Error(e) = start_num {
return types::Value::Error(e);
}
let find_s = match value_to_string_for_find(&find_text) {
CoerceForFind::Coerced(s) => s,
CoerceForFind::Propagate(v) => return v,
};
let within_s = match value_to_string_for_find(&within_text) {
CoerceForFind::Coerced(s) => s,
CoerceForFind::Propagate(v) => return v,
};
let start_i64: i64 = match &start_num {
types::Value::Number(n) => n.as_(),
_ => return types::Value::Error(Error::Value),
};
if start_i64 <= 0 {
return types::Value::Error(Error::Value);
}
let within_len = within_s.chars().count() as i64;
if start_i64 > within_len {
return types::Value::Error(Error::Value);
}
match find_position_case_sensitive(&find_s, &within_s, start_i64) {
Some(pos) => N::from_i64(pos)
.or_else(|| N::from_f64(1.0))
.map(types::Value::Number)
.unwrap_or_else(|| types::Value::Error(Error::Value)),
None => types::Value::Error(Error::Value),
}
}

fn calculate_search<N>(
(find_text, within_text, start_num): (types::Value<N>, types::Value<N>, types::Value<N>),
) -> types::Value<N>
where
N: XlNum,
<N as FromStr>::Err: Debug,
{
if let types::Value::Error(e) = find_text {
return types::Value::Error(e);
}
if let types::Value::Error(e) = within_text {
return types::Value::Error(e);
}
if let types::Value::Error(e) = start_num {
return types::Value::Error(e);
}
let find_s = match value_to_string_for_find(&find_text) {
CoerceForFind::Coerced(s) => s,
CoerceForFind::Propagate(v) => return v,
};
let within_s = match value_to_string_for_find(&within_text) {
CoerceForFind::Coerced(s) => s,
CoerceForFind::Propagate(v) => return v,
};
let start_i64: i64 = match &start_num {
types::Value::Number(n) => n.as_(),
_ => return types::Value::Error(Error::Value),
};
if start_i64 <= 0 {
return types::Value::Error(Error::Value);
}
let within_len = within_s.chars().count() as i64;
if start_i64 > within_len {
return types::Value::Error(Error::Value);
}
match search_position_with_wildcards(&find_s, &within_s, start_i64) {
Some(pos) => N::from_i64(pos)
.or_else(|| N::from_f64(1.0))
.map(types::Value::Number)
.unwrap_or_else(|| types::Value::Error(Error::Value)),
None => types::Value::Error(Error::Value),
}
}

fn calculate_iserror<N>(arg: types::Value<N>) -> types::Value<N>
Comment thread
benkleintechnologies marked this conversation as resolved.
where
N: XlNum,
{
types::Value::Boolean(matches!(arg, types::Value::Error(_)).into())
}
164 changes: 164 additions & 0 deletions src/calculate/operation/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,167 @@ where
{
types::Value::Boolean(f(string1, string2).into())
}

/// Result of coercing a formula value to string for FIND/SEARCH (Excel semantics).
#[derive(Debug, Clone)]
pub enum CoerceForFind<N>
where
N: XlNum,
{
/// Value was coerced to string; use for FIND/SEARCH.
Coerced(String),
/// Propagate this value as the formula result (e.g. existing error or #VALUE! for Date/Iterator).
Propagate(types::Value<N>),
}

/// Coerce a formula value to string for FIND/SEARCH (Excel semantics).
pub fn value_to_string_for_find<N>(v: &types::Value<N>) -> CoerceForFind<N>
where
N: XlNum,
<N as FromStr>::Err: Debug,
{
match v {
types::Value::Error(e) => CoerceForFind::Propagate(types::Value::Error(*e)),
types::Value::Number(n) => CoerceForFind::Coerced(n.to_string()),
types::Value::Text(s) => CoerceForFind::Coerced(s.clone()),
types::Value::Boolean(b) => CoerceForFind::Coerced(b.to_string()),
types::Value::Blank => CoerceForFind::Coerced(String::new()),
types::Value::Date(_) | types::Value::Iterator(_) => {
CoerceForFind::Propagate(types::Value::Error(types::Error::Value))
}
}
}

/// FIND: case-sensitive, no wildcards. Returns 1-based character position or None.
pub fn find_position_case_sensitive(
find_text: &str,
within_text: &str,
start_num_1based: i64,
) -> Option<i64> {
let start: usize = start_num_1based.checked_sub(1)?.try_into().ok()?;
// Character count (not byte len) for Excel 1-based character position semantics and UTF-8.
let char_count = within_text.chars().count();
Comment thread
ben-schreiber marked this conversation as resolved.
if start >= char_count {
return None;
}
if find_text.is_empty() {
return Some(start_num_1based);
}
let rest: String = within_text.chars().skip(start).collect();
let byte_offset = rest.find(find_text)?;
let chars_before = rest[..byte_offset].chars().count();
let one_based = (start + chars_before + 1) as i64;
Some(one_based)
}

/// SEARCH: case-insensitive with wildcards ? * and ~ escape. Returns 1-based character position or None.
pub fn search_position_with_wildcards(
find_text: &str,
within_text: &str,
start_num_1based: i64,
) -> Option<i64> {
let start: usize = start_num_1based.checked_sub(1)?.try_into().ok()?;
// Character count (not byte len) for Excel 1-based character position semantics and UTF-8.
let char_count = within_text.chars().count();
if start >= char_count {
return None;
}
if find_text.is_empty() {
return Some(start_num_1based);
}
let rest: String = within_text.chars().skip(start).collect();
let rest_lower: String = rest.to_lowercase();
let pattern_orig: Vec<char> = find_text.chars().collect();
let pattern_lower: Vec<char> = find_text.to_lowercase().chars().collect();
let rest_orig: Vec<char> = rest.chars().collect();
let rest_chars: Vec<char> = rest_lower.chars().collect();
let mut best: Option<usize> = None;
for i in 0..=rest_chars.len() {
if let Some(offset) = match_pattern(
&pattern_lower,
&pattern_orig,
&rest_chars,
&rest_orig,
i,
0,
i,
) {
let pos = start + offset + 1;
best = Some(match best {
None => pos,
Some(p) => p.min(pos),
});
}
}
best.map(|p| p as i64)
}

/// Match pattern (with ? * ~) against text from position. Returns Some(offset) where offset
/// is the 0-based position in text where the matched substring starts (for leading *, this
/// is where the remainder matches, not where * started).
fn match_pattern(
pattern_lower: &[char],
pattern_orig: &[char],
text_lower: &[char],
text_orig: &[char],
text_pos: usize,
pattern_pos: usize,
match_start: usize,
) -> Option<usize> {
if pattern_pos >= pattern_lower.len() {
return Some(match_start);
}
let mut p = pattern_pos;
let mut t = text_pos;
while p < pattern_lower.len() {
if t > text_lower.len() {
return None;
}
if p + 1 < pattern_orig.len() && pattern_orig[p] == '~' {
let next = pattern_orig[p + 1];
if next == '?' || next == '*' || next == '~' {
if t >= text_orig.len() || text_orig[t] != next {
return None;
}
p += 2;
t += 1;
continue;
}
}
match pattern_lower[p] {
'?' => {
if t >= text_lower.len() {
return None;
}
p += 1;
t += 1;
}
'*' => {
let rest_pat_lower = &pattern_lower[p + 1..];
let rest_pat_orig = &pattern_orig[p + 1..];
for skip in 0..=text_lower.len().saturating_sub(t) {
if let Some(off) = match_pattern(
rest_pat_lower,
rest_pat_orig,
text_lower,
text_orig,
t + skip,
0,
t + skip,
) {
return Some(off);
}
}
return None;
}
c => {
if t >= text_lower.len() || text_lower[t] != c {
return None;
}
p += 1;
t += 1;
}
}
}
Some(match_start)
}
5 changes: 4 additions & 1 deletion src/grammar.pest
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ logical_operator = _{ equal | not_equal | greater_or_equal | greater
greater_or_equal= { ">=" }
less_or_equal = { "<=" }

function = _{ abs | sum | product | average | negate | days | year | month | day | right | left | iff | isblank | custom_function }
function = _{ abs | sum | product | average | negate | days | year | month | day | right | left | iff | isblank | find | search | iserror | custom_function }
abs = { ^"ABS" ~ "(" ~ expr ~ ")" }
sum = { ^"SUM" ~ function_param_with_atomic_expr}
product = { ^"PRODUCT" ~ function_param_with_atomic_expr}
Expand All @@ -50,6 +50,9 @@ function = _{ abs | sum | product | average | negate | days | year | month | day
left = { ^"LEFT" ~ function_param }
iff = { ^"IF" ~ three_params }
isblank = { ^"ISBLANK" ~ function_param}
find = { ^"FIND" ~ function_param }
search = { ^"SEARCH" ~ function_param }
iserror = { ^"ISERROR" ~ function_param }
custom_function = { reference ~ (function_param | empty_param) }

logical_function = _{ or | and | xor | not }
Expand Down
6 changes: 6 additions & 0 deletions src/parse_formula.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ fn rule_to_function_operator(collective_operation: Rule) -> types::Operator {
Rule::year => types::Operator::Function(types::Function::Year),
Rule::month => types::Operator::Function(types::Function::Month),
Rule::day => types::Operator::Function(types::Function::Day),
Rule::find => types::Operator::Function(types::Function::Find),
Rule::search => types::Operator::Function(types::Function::Search),
Rule::iserror => types::Operator::Function(types::Function::IsError),
_ => unreachable!(),
}
}
Expand Down Expand Up @@ -404,6 +407,9 @@ where
Rule::custom_function => build_formula_custom_function(pair, f),
Rule::iff => build_formula_iff(pair, f),
Rule::isblank => build_formula_collective_operator(Rule::isblank, pair, f),
Rule::find => build_formula_collective_operator(Rule::find, pair, f),
Rule::search => build_formula_collective_operator(Rule::search, pair, f),
Rule::iserror => build_formula_collective_operator(Rule::iserror, pair, f),
_ => unreachable!(),
})
.map_infix(
Expand Down
3 changes: 3 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub enum Function {
Year,
Month,
Day,
Find,
Search,
IsError,
}

/// Defines Excel Operators.
Expand Down
Loading