formualizer_eval/builtins/text/
find_search_exact.rs1use super::super::utils::ARG_ANY_ONE;
2use crate::args::ArgSchema;
3use crate::function::Function;
4use crate::traits::{ArgumentHandle, FunctionContext};
5use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
6use formualizer_macros::func_caps;
7
8fn scalar_like_value(arg: &ArgumentHandle<'_, '_>) -> Result<LiteralValue, ExcelError> {
9 Ok(match arg.value()? {
10 crate::traits::CalcValue::Scalar(v) => v,
11 crate::traits::CalcValue::Range(rv) => rv.get_cell(0, 0),
12 crate::traits::CalcValue::Callable(_) => LiteralValue::Error(
13 ExcelError::new(ExcelErrorKind::Calc).with_message("LAMBDA value must be invoked"),
14 ),
15 })
16}
17
18fn to_text<'a, 'b>(a: &ArgumentHandle<'a, 'b>) -> Result<String, ExcelError> {
19 let v = scalar_like_value(a)?;
20 Ok(match v {
21 LiteralValue::Text(s) => s,
22 LiteralValue::Empty => String::new(),
23 LiteralValue::Boolean(b) => {
24 if b {
25 "TRUE".into()
26 } else {
27 "FALSE".into()
28 }
29 }
30 LiteralValue::Int(i) => i.to_string(),
31 LiteralValue::Number(f) => f.to_string(),
32 LiteralValue::Error(e) => return Err(e),
33 other => other.to_string(),
34 })
35}
36
37#[derive(Debug)]
39pub struct FindFn;
40impl Function for FindFn {
84 func_caps!(PURE);
85 fn name(&self) -> &'static str {
86 "FIND"
87 }
88 fn min_args(&self) -> usize {
89 2
90 }
91 fn variadic(&self) -> bool {
92 true
93 }
94 fn arg_schema(&self) -> &'static [ArgSchema] {
95 &ARG_ANY_ONE[..]
96 }
97 fn eval<'a, 'b, 'c>(
98 &self,
99 args: &'c [ArgumentHandle<'a, 'b>],
100 _: &dyn FunctionContext<'b>,
101 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
102 if args.len() < 2 || args.len() > 3 {
103 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
104 ExcelError::new_value(),
105 )));
106 }
107 let needle = to_text(&args[0])?;
108 let hay = to_text(&args[1])?;
109 let start = if args.len() == 3 {
110 let n = number_like(&args[2])?;
111 if n < 1 {
112 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
113 ExcelError::new_value(),
114 )));
115 }
116 (n - 1) as usize
117 } else {
118 0
119 };
120 if needle.is_empty() {
121 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(1)));
122 }
123 match char_find(&hay, &needle, start) {
126 Some(idx) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
127 (idx + 1) as i64,
128 ))),
129 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
130 ExcelError::new_value(),
131 ))),
132 }
133 }
134}
135
136#[derive(Debug)]
138pub struct SearchFn;
139impl Function for SearchFn {
183 func_caps!(PURE);
184 fn name(&self) -> &'static str {
185 "SEARCH"
186 }
187 fn min_args(&self) -> usize {
188 2
189 }
190 fn variadic(&self) -> bool {
191 true
192 }
193 fn arg_schema(&self) -> &'static [ArgSchema] {
194 &ARG_ANY_ONE[..]
195 }
196 fn eval<'a, 'b, 'c>(
197 &self,
198 args: &'c [ArgumentHandle<'a, 'b>],
199 _: &dyn FunctionContext<'b>,
200 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
201 if args.len() < 2 || args.len() > 3 {
202 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
203 ExcelError::new_value(),
204 )));
205 }
206 let needle = to_text(&args[0])?.to_ascii_lowercase();
207 let hay_raw = to_text(&args[1])?;
208 let hay = hay_raw.to_ascii_lowercase();
209 let start = if args.len() == 3 {
210 let n = number_like(&args[2])?;
211 if n < 1 {
212 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
213 ExcelError::new_value(),
214 )));
215 }
216 (n - 1) as usize
217 } else {
218 0
219 };
220 if needle.is_empty() {
221 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(1)));
222 }
223 let hay_chars: Vec<char> = hay.chars().collect();
227 if start > hay_chars.len() {
228 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
229 ExcelError::new_value(),
230 )));
231 }
232 let found = if needle.contains('*') || needle.contains('?') {
233 let pat: Vec<char> = needle.chars().collect();
234 char_wildcard_search(&pat, &hay_chars, start)
235 } else {
236 char_find(&hay, &needle, start)
237 };
238 match found {
239 Some(idx) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
240 (idx + 1) as i64,
241 ))),
242 None => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
243 ExcelError::from_error_string("#VALUE!"),
244 ))),
245 }
246 }
247}
248
249fn char_find(hay: &str, needle: &str, start: usize) -> Option<usize> {
253 let hay_chars: Vec<char> = hay.chars().collect();
254 let needle_chars: Vec<char> = needle.chars().collect();
255 if needle_chars.is_empty() {
256 return Some(start.min(hay_chars.len()));
257 }
258 if needle_chars.len() > hay_chars.len() || start > hay_chars.len() {
259 return None;
260 }
261 let last = hay_chars.len() - needle_chars.len();
262 let mut i = start;
263 while i <= last {
264 if hay_chars[i..i + needle_chars.len()] == needle_chars[..] {
265 return Some(i);
266 }
267 i += 1;
268 }
269 None
270}
271
272fn char_wildcard_search(pat: &[char], hay: &[char], start: usize) -> Option<usize> {
274 let mut i = start;
275 while i <= hay.len() {
276 if wildcard_match_chars(pat, &hay[i..]) {
277 return Some(i);
278 }
279 i += 1;
280 }
281 None
282}
283
284fn wildcard_match_chars(p: &[char], t: &[char]) -> bool {
285 if p.is_empty() {
286 return true;
287 }
288 match p[0] {
289 '*' => (0..=t.len()).any(|i| wildcard_match_chars(&p[1..], &t[i..])),
290 '?' => !t.is_empty() && wildcard_match_chars(&p[1..], &t[1..]),
291 c => !t.is_empty() && t[0] == c && wildcard_match_chars(&p[1..], &t[1..]),
292 }
293}
294
295#[derive(Debug)]
297pub struct ExactFn;
298impl Function for ExactFn {
340 func_caps!(PURE);
341 fn name(&self) -> &'static str {
342 "EXACT"
343 }
344 fn min_args(&self) -> usize {
345 2
346 }
347 fn arg_schema(&self) -> &'static [ArgSchema] {
348 &ARG_ANY_ONE[..]
349 }
350 fn eval<'a, 'b, 'c>(
351 &self,
352 args: &'c [ArgumentHandle<'a, 'b>],
353 _: &dyn FunctionContext<'b>,
354 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
355 let a = to_text(&args[0])?;
356 let b = to_text(&args[1])?;
357 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Boolean(
358 a == b,
359 )))
360 }
361}
362
363fn number_like<'a, 'b>(a: &ArgumentHandle<'a, 'b>) -> Result<i64, ExcelError> {
364 let v = scalar_like_value(a)?;
365 Ok(match v {
366 LiteralValue::Int(i) => i,
367 LiteralValue::Number(f) => f as i64,
368 LiteralValue::Text(t) => t.parse::<i64>().unwrap_or(0),
369 LiteralValue::Boolean(b) => {
370 if b {
371 1
372 } else {
373 0
374 }
375 }
376 LiteralValue::Empty => 0,
377 LiteralValue::Error(e) => return Err(e),
378 other => other.to_string().parse::<i64>().unwrap_or(0),
379 })
380}
381
382pub fn register_builtins() {
383 use std::sync::Arc;
384 crate::function_registry::register_function(Arc::new(FindFn));
385 crate::function_registry::register_function(Arc::new(SearchFn));
386 crate::function_registry::register_function(Arc::new(ExactFn));
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392 use crate::test_workbook::TestWorkbook;
393 use crate::traits::ArgumentHandle;
394 use formualizer_common::LiteralValue;
395 use formualizer_parse::parser::{ASTNode, ASTNodeType};
396 fn lit(v: LiteralValue) -> ASTNode {
397 ASTNode::new(ASTNodeType::Literal(v), None)
398 }
399 #[test]
400 fn find_search() {
401 let wb = TestWorkbook::new()
402 .with_function(std::sync::Arc::new(FindFn))
403 .with_function(std::sync::Arc::new(SearchFn));
404 let ctx = wb.interpreter();
405 let f = ctx.context.get_function("", "FIND").unwrap();
406 let s = ctx.context.get_function("", "SEARCH").unwrap();
407 let hay = lit(LiteralValue::Text("Hello World".into()));
408 let needle = lit(LiteralValue::Text("World".into()));
409 assert_eq!(
410 f.dispatch(
411 &[
412 ArgumentHandle::new(&needle, &ctx),
413 ArgumentHandle::new(&hay, &ctx)
414 ],
415 &ctx.function_context(None)
416 )
417 .unwrap()
418 .into_literal(),
419 LiteralValue::Int(7)
420 );
421 let needle2 = lit(LiteralValue::Text("world".into()));
422 assert_eq!(
423 s.dispatch(
424 &[
425 ArgumentHandle::new(&needle2, &ctx),
426 ArgumentHandle::new(&hay, &ctx)
427 ],
428 &ctx.function_context(None)
429 )
430 .unwrap()
431 .into_literal(),
432 LiteralValue::Int(7)
433 );
434 }
435
436 #[test]
441 fn find_search_utf8_char_positions() {
442 let wb = TestWorkbook::new()
443 .with_function(std::sync::Arc::new(FindFn))
444 .with_function(std::sync::Arc::new(SearchFn));
445 let ctx = wb.interpreter();
446 let f = ctx.context.get_function("", "FIND").unwrap();
447 let s = ctx.context.get_function("", "SEARCH").unwrap();
448 let call =
449 |func: &std::sync::Arc<dyn crate::function::Function>, needle: &str, hay: &str| {
450 let n = lit(LiteralValue::Text(needle.into()));
451 let h = lit(LiteralValue::Text(hay.into()));
452 func.dispatch(
453 &[ArgumentHandle::new(&n, &ctx), ArgumentHandle::new(&h, &ctx)],
454 &ctx.function_context(None),
455 )
456 .unwrap()
457 .into_literal()
458 };
459
460 assert_eq!(call(&f, "z", "éz"), LiteralValue::Int(2));
463
464 assert_eq!(call(&s, "?z", "éz"), LiteralValue::Int(1));
467
468 assert_eq!(call(&s, "c?fé", "cafés"), LiteralValue::Int(1));
470 }
471}