use super::{super::utils::ARG_ANY_ONE, scalar_text_value};
use crate::args::ArgSchema;
use crate::function::Function;
use crate::traits::{ArgumentHandle, FunctionContext};
use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
use formualizer_macros::func_caps;
fn scalar_like_value(arg: &ArgumentHandle<'_, '_>) -> Result<LiteralValue, ExcelError> {
Ok(match arg.value()? {
crate::traits::CalcValue::Scalar(v) => v,
crate::traits::CalcValue::Range(rv) => rv.get_cell(0, 0),
crate::traits::CalcValue::Callable(_) => LiteralValue::Error(
ExcelError::new(ExcelErrorKind::Calc).with_message("LAMBDA value must be invoked"),
),
})
}
fn to_text<'a, 'b>(a: &ArgumentHandle<'a, 'b>) -> Result<String, ExcelError> {
let v = scalar_text_value(a)?;
Ok(match v {
LiteralValue::Text(s) => s,
LiteralValue::Empty => String::new(),
LiteralValue::Boolean(b) => {
if b {
"TRUE".into()
} else {
"FALSE".into()
}
}
LiteralValue::Int(i) => i.to_string(),
LiteralValue::Number(f) => f.to_string(),
LiteralValue::Error(e) => return Err(e),
other => other.to_string(),
})
}
#[derive(Debug)]
pub struct FindFn;
impl Function for FindFn {
func_caps!(PURE);
fn name(&self) -> &'static str {
"FIND"
}
fn min_args(&self) -> usize {
2
}
fn variadic(&self) -> bool {
true
}
fn arg_schema(&self) -> &'static [ArgSchema] {
&ARG_ANY_ONE[..]
}
fn eval<'a, 'b, 'c>(
&self,
args: &'c [ArgumentHandle<'a, 'b>],
_: &dyn FunctionContext<'b>,
) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
if args.len() < 2 || args.len() > 3 {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
ExcelError::new_value(),
)));
}
let needle = to_text(&args[0])?;
let hay = to_text(&args[1])?;
let start = if args.len() == 3 {
let n = number_like(&args[2])?;
if n < 1 {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
ExcelError::new_value(),
)));
}
(n - 1) as usize
} else {
0
};
if needle.is_empty() {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(1)));
}
match char_find(&hay, &needle, start) {
Some(idx) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
(idx + 1) as i64,
))),
None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
ExcelError::new_value(),
))),
}
}
}
#[derive(Debug)]
pub struct SearchFn;
impl Function for SearchFn {
func_caps!(PURE);
fn name(&self) -> &'static str {
"SEARCH"
}
fn min_args(&self) -> usize {
2
}
fn variadic(&self) -> bool {
true
}
fn arg_schema(&self) -> &'static [ArgSchema] {
&ARG_ANY_ONE[..]
}
fn eval<'a, 'b, 'c>(
&self,
args: &'c [ArgumentHandle<'a, 'b>],
_: &dyn FunctionContext<'b>,
) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
if args.len() < 2 || args.len() > 3 {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
ExcelError::new_value(),
)));
}
let needle = to_text(&args[0])?.to_ascii_lowercase();
let hay_raw = to_text(&args[1])?;
let hay = hay_raw.to_ascii_lowercase();
let start = if args.len() == 3 {
let n = number_like(&args[2])?;
if n < 1 {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
ExcelError::new_value(),
)));
}
(n - 1) as usize
} else {
0
};
if needle.is_empty() {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(1)));
}
let hay_chars: Vec<char> = hay.chars().collect();
if start > hay_chars.len() {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
ExcelError::new_value(),
)));
}
let found = if needle.contains('*') || needle.contains('?') {
let pat: Vec<char> = needle.chars().collect();
char_wildcard_search(&pat, &hay_chars, start)
} else {
char_find(&hay, &needle, start)
};
match found {
Some(idx) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
(idx + 1) as i64,
))),
None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
ExcelError::from_error_string("#VALUE!"),
))),
}
}
}
fn char_find(hay: &str, needle: &str, start: usize) -> Option<usize> {
let hay_chars: Vec<char> = hay.chars().collect();
let needle_chars: Vec<char> = needle.chars().collect();
if needle_chars.is_empty() {
return Some(start.min(hay_chars.len()));
}
if needle_chars.len() > hay_chars.len() || start > hay_chars.len() {
return None;
}
let last = hay_chars.len() - needle_chars.len();
let mut i = start;
while i <= last {
if hay_chars[i..i + needle_chars.len()] == needle_chars[..] {
return Some(i);
}
i += 1;
}
None
}
fn char_wildcard_search(pat: &[char], hay: &[char], start: usize) -> Option<usize> {
let mut i = start;
while i <= hay.len() {
if wildcard_match_chars(pat, &hay[i..]) {
return Some(i);
}
i += 1;
}
None
}
fn wildcard_match_chars(p: &[char], t: &[char]) -> bool {
if p.is_empty() {
return true;
}
match p[0] {
'*' => (0..=t.len()).any(|i| wildcard_match_chars(&p[1..], &t[i..])),
'?' => !t.is_empty() && wildcard_match_chars(&p[1..], &t[1..]),
c => !t.is_empty() && t[0] == c && wildcard_match_chars(&p[1..], &t[1..]),
}
}
#[derive(Debug)]
pub struct ExactFn;
impl Function for ExactFn {
func_caps!(PURE);
fn name(&self) -> &'static str {
"EXACT"
}
fn min_args(&self) -> usize {
2
}
fn arg_schema(&self) -> &'static [ArgSchema] {
&ARG_ANY_ONE[..]
}
fn eval<'a, 'b, 'c>(
&self,
args: &'c [ArgumentHandle<'a, 'b>],
_: &dyn FunctionContext<'b>,
) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
let a = to_text(&args[0])?;
let b = to_text(&args[1])?;
Ok(crate::traits::CalcValue::Scalar(LiteralValue::Boolean(
a == b,
)))
}
}
fn number_like<'a, 'b>(a: &ArgumentHandle<'a, 'b>) -> Result<i64, ExcelError> {
let v = scalar_like_value(a)?;
Ok(match v {
LiteralValue::Int(i) => i,
LiteralValue::Number(f) => f as i64,
LiteralValue::Text(t) => t.parse::<i64>().unwrap_or(0),
LiteralValue::Boolean(b) => {
if b {
1
} else {
0
}
}
LiteralValue::Empty => 0,
LiteralValue::Error(e) => return Err(e),
other => other.to_string().parse::<i64>().unwrap_or(0),
})
}
pub fn register_builtins() {
use std::sync::Arc;
crate::function_registry::register_builtin(Arc::new(FindFn));
crate::function_registry::register_builtin(Arc::new(SearchFn));
crate::function_registry::register_builtin(Arc::new(ExactFn));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_workbook::TestWorkbook;
use crate::traits::ArgumentHandle;
use formualizer_common::LiteralValue;
use formualizer_parse::parser::{ASTNode, ASTNodeType};
fn lit(v: LiteralValue) -> ASTNode {
ASTNode::new(ASTNodeType::Literal(v), None)
}
#[test]
fn find_search() {
let wb = TestWorkbook::new()
.with_function(std::sync::Arc::new(FindFn))
.with_function(std::sync::Arc::new(SearchFn));
let ctx = wb.interpreter();
let f = ctx.context.get_function("", "FIND").unwrap();
let s = ctx.context.get_function("", "SEARCH").unwrap();
let hay = lit(LiteralValue::Text("Hello World".into()));
let needle = lit(LiteralValue::Text("World".into()));
assert_eq!(
f.dispatch(
&[
ArgumentHandle::new(&needle, &ctx),
ArgumentHandle::new(&hay, &ctx)
],
&ctx.function_context(None)
)
.unwrap()
.into_literal(),
LiteralValue::Int(7)
);
let needle2 = lit(LiteralValue::Text("world".into()));
assert_eq!(
s.dispatch(
&[
ArgumentHandle::new(&needle2, &ctx),
ArgumentHandle::new(&hay, &ctx)
],
&ctx.function_context(None)
)
.unwrap()
.into_literal(),
LiteralValue::Int(7)
);
}
#[test]
fn find_search_utf8_char_positions() {
let wb = TestWorkbook::new()
.with_function(std::sync::Arc::new(FindFn))
.with_function(std::sync::Arc::new(SearchFn));
let ctx = wb.interpreter();
let f = ctx.context.get_function("", "FIND").unwrap();
let s = ctx.context.get_function("", "SEARCH").unwrap();
let call =
|func: &std::sync::Arc<dyn crate::function::Function>, needle: &str, hay: &str| {
let n = lit(LiteralValue::Text(needle.into()));
let h = lit(LiteralValue::Text(hay.into()));
func.dispatch(
&[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&h, &ctx)],
&ctx.function_context(None),
)
.unwrap()
.into_literal()
};
assert_eq!(call(&f, "z", "éz"), LiteralValue::Int(2));
assert_eq!(call(&s, "?z", "éz"), LiteralValue::Int(1));
assert_eq!(call(&s, "c?fé", "cafés"), LiteralValue::Int(1));
}
}