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 if start > hay.len() {
124 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
125 ExcelError::new_value(),
126 )));
127 }
128 if let Some(pos) = hay[start..].find(&needle) {
129 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
130 (start + pos + 1) as i64,
131 )))
132 } else {
133 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
134 ExcelError::new_value(),
135 )))
136 }
137 }
138}
139
140#[derive(Debug)]
142pub struct SearchFn;
143impl Function for SearchFn {
187 func_caps!(PURE);
188 fn name(&self) -> &'static str {
189 "SEARCH"
190 }
191 fn min_args(&self) -> usize {
192 2
193 }
194 fn variadic(&self) -> bool {
195 true
196 }
197 fn arg_schema(&self) -> &'static [ArgSchema] {
198 &ARG_ANY_ONE[..]
199 }
200 fn eval<'a, 'b, 'c>(
201 &self,
202 args: &'c [ArgumentHandle<'a, 'b>],
203 _: &dyn FunctionContext<'b>,
204 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
205 if args.len() < 2 || args.len() > 3 {
206 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
207 ExcelError::new_value(),
208 )));
209 }
210 let needle = to_text(&args[0])?.to_ascii_lowercase();
211 let hay_raw = to_text(&args[1])?;
212 let hay = hay_raw.to_ascii_lowercase();
213 let start = if args.len() == 3 {
214 let n = number_like(&args[2])?;
215 if n < 1 {
216 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
217 ExcelError::new_value(),
218 )));
219 }
220 (n - 1) as usize
221 } else {
222 0
223 };
224 if needle.is_empty() {
225 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(1)));
226 }
227 if start > hay.len() {
228 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
229 ExcelError::new_value(),
230 )));
231 }
232 let is_wild = needle.contains('*') || needle.contains('?');
235 if !is_wild {
236 if let Some(pos) = hay[start..].find(&needle) {
237 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
238 (start + pos + 1) as i64,
239 )));
240 } else {
241 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
242 ExcelError::new_value(),
243 )));
244 }
245 }
246 for offset in start..=hay.len() {
248 if wildcard_match(&needle, &hay[offset..]) {
249 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
250 (offset + 1) as i64,
251 )));
252 }
253 }
254 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
255 ExcelError::from_error_string("#VALUE!"),
256 )))
257 }
258}
259
260fn wildcard_match(pat: &str, text: &str) -> bool {
261 fn rec(p: &[u8], t: &[u8]) -> bool {
262 if p.is_empty() {
263 return true;
264 }
265 match p[0] {
266 b'*' => {
267 for i in 0..=t.len() {
268 if rec(&p[1..], &t[i..]) {
269 return true;
270 }
271 }
272 false
273 }
274 b'?' => {
275 if t.is_empty() {
276 false
277 } else {
278 rec(&p[1..], &t[1..])
279 }
280 }
281 c => {
282 if !t.is_empty() && t[0] == c {
283 rec(&p[1..], &t[1..])
284 } else {
285 false
286 }
287 }
288 }
289 }
290 rec(pat.as_bytes(), text.as_bytes())
291}
292
293#[derive(Debug)]
295pub struct ExactFn;
296impl Function for ExactFn {
338 func_caps!(PURE);
339 fn name(&self) -> &'static str {
340 "EXACT"
341 }
342 fn min_args(&self) -> usize {
343 2
344 }
345 fn arg_schema(&self) -> &'static [ArgSchema] {
346 &ARG_ANY_ONE[..]
347 }
348 fn eval<'a, 'b, 'c>(
349 &self,
350 args: &'c [ArgumentHandle<'a, 'b>],
351 _: &dyn FunctionContext<'b>,
352 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
353 let a = to_text(&args[0])?;
354 let b = to_text(&args[1])?;
355 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Boolean(
356 a == b,
357 )))
358 }
359}
360
361fn number_like<'a, 'b>(a: &ArgumentHandle<'a, 'b>) -> Result<i64, ExcelError> {
362 let v = scalar_like_value(a)?;
363 Ok(match v {
364 LiteralValue::Int(i) => i,
365 LiteralValue::Number(f) => f as i64,
366 LiteralValue::Text(t) => t.parse::<i64>().unwrap_or(0),
367 LiteralValue::Boolean(b) => {
368 if b {
369 1
370 } else {
371 0
372 }
373 }
374 LiteralValue::Empty => 0,
375 LiteralValue::Error(e) => return Err(e),
376 other => other.to_string().parse::<i64>().unwrap_or(0),
377 })
378}
379
380pub fn register_builtins() {
381 use std::sync::Arc;
382 crate::function_registry::register_function(Arc::new(FindFn));
383 crate::function_registry::register_function(Arc::new(SearchFn));
384 crate::function_registry::register_function(Arc::new(ExactFn));
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use crate::test_workbook::TestWorkbook;
391 use crate::traits::ArgumentHandle;
392 use formualizer_common::LiteralValue;
393 use formualizer_parse::parser::{ASTNode, ASTNodeType};
394 fn lit(v: LiteralValue) -> ASTNode {
395 ASTNode::new(ASTNodeType::Literal(v), None)
396 }
397 #[test]
398 fn find_search() {
399 let wb = TestWorkbook::new()
400 .with_function(std::sync::Arc::new(FindFn))
401 .with_function(std::sync::Arc::new(SearchFn));
402 let ctx = wb.interpreter();
403 let f = ctx.context.get_function("", "FIND").unwrap();
404 let s = ctx.context.get_function("", "SEARCH").unwrap();
405 let hay = lit(LiteralValue::Text("Hello World".into()));
406 let needle = lit(LiteralValue::Text("World".into()));
407 assert_eq!(
408 f.dispatch(
409 &[
410 ArgumentHandle::new(&needle, &ctx),
411 ArgumentHandle::new(&hay, &ctx)
412 ],
413 &ctx.function_context(None)
414 )
415 .unwrap()
416 .into_literal(),
417 LiteralValue::Int(7)
418 );
419 let needle2 = lit(LiteralValue::Text("world".into()));
420 assert_eq!(
421 s.dispatch(
422 &[
423 ArgumentHandle::new(&needle2, &ctx),
424 ArgumentHandle::new(&hay, &ctx)
425 ],
426 &ctx.function_context(None)
427 )
428 .unwrap()
429 .into_literal(),
430 LiteralValue::Int(7)
431 );
432 }
433}