formualizer_eval/builtins/text/
find_search_exact.rs

1use super::super::utils::ARG_ANY_ONE;
2use crate::args::ArgSchema;
3use crate::function::Function;
4use crate::traits::{ArgumentHandle, FunctionContext};
5use formualizer_common::{ExcelError, LiteralValue};
6use formualizer_macros::func_caps;
7
8fn to_text<'a, 'b>(a: &ArgumentHandle<'a, 'b>) -> Result<String, ExcelError> {
9    let v = a.value()?;
10    Ok(match v.as_ref() {
11        LiteralValue::Text(s) => s.clone(),
12        LiteralValue::Empty => String::new(),
13        LiteralValue::Boolean(b) => {
14            if *b {
15                "TRUE".into()
16            } else {
17                "FALSE".into()
18            }
19        }
20        LiteralValue::Int(i) => i.to_string(),
21        LiteralValue::Number(f) => f.to_string(),
22        LiteralValue::Error(e) => return Err(e.clone()),
23        other => other.to_string(),
24    })
25}
26
27// FIND(find_text, within_text, [start_num]) - case sensitive
28#[derive(Debug)]
29pub struct FindFn;
30impl Function for FindFn {
31    func_caps!(PURE);
32    fn name(&self) -> &'static str {
33        "FIND"
34    }
35    fn min_args(&self) -> usize {
36        2
37    }
38    fn variadic(&self) -> bool {
39        true
40    }
41    fn arg_schema(&self) -> &'static [ArgSchema] {
42        &ARG_ANY_ONE[..]
43    }
44    fn eval_scalar<'a, 'b>(
45        &self,
46        args: &'a [ArgumentHandle<'a, 'b>],
47        _: &dyn FunctionContext,
48    ) -> Result<LiteralValue, ExcelError> {
49        if args.len() < 2 || args.len() > 3 {
50            return Ok(LiteralValue::Error(ExcelError::new_value()));
51        }
52        let needle = to_text(&args[0])?;
53        let hay = to_text(&args[1])?;
54        let start = if args.len() == 3 {
55            let n = number_like(&args[2])?;
56            if n < 1 {
57                return Ok(LiteralValue::Error(ExcelError::new_value()));
58            }
59            (n - 1) as usize
60        } else {
61            0
62        };
63        if needle.is_empty() {
64            return Ok(LiteralValue::Int(1));
65        }
66        if start > hay.len() {
67            return Ok(LiteralValue::Error(ExcelError::new_value()));
68        }
69        if let Some(pos) = hay[start..].find(&needle) {
70            Ok(LiteralValue::Int((start + pos + 1) as i64))
71        } else {
72            Ok(LiteralValue::Error(ExcelError::new_value()))
73        }
74    }
75}
76
77// SEARCH(find_text, within_text, [start_num]) - case insensitive + simple wildcard * ?
78#[derive(Debug)]
79pub struct SearchFn;
80impl Function for SearchFn {
81    func_caps!(PURE);
82    fn name(&self) -> &'static str {
83        "SEARCH"
84    }
85    fn min_args(&self) -> usize {
86        2
87    }
88    fn variadic(&self) -> bool {
89        true
90    }
91    fn arg_schema(&self) -> &'static [ArgSchema] {
92        &ARG_ANY_ONE[..]
93    }
94    fn eval_scalar<'a, 'b>(
95        &self,
96        args: &'a [ArgumentHandle<'a, 'b>],
97        _: &dyn FunctionContext,
98    ) -> Result<LiteralValue, ExcelError> {
99        if args.len() < 2 || args.len() > 3 {
100            return Ok(LiteralValue::Error(ExcelError::new_value()));
101        }
102        let needle = to_text(&args[0])?.to_ascii_lowercase();
103        let hay_raw = to_text(&args[1])?;
104        let hay = hay_raw.to_ascii_lowercase();
105        let start = if args.len() == 3 {
106            let n = number_like(&args[2])?;
107            if n < 1 {
108                return Ok(LiteralValue::Error(ExcelError::new_value()));
109            }
110            (n - 1) as usize
111        } else {
112            0
113        };
114        if needle.is_empty() {
115            return Ok(LiteralValue::Int(1));
116        }
117        if start > hay.len() {
118            return Ok(LiteralValue::Error(ExcelError::new_value()));
119        }
120        // Convert wildcard to regex-like simple pattern
121        // We'll implement manual scanning.
122        let is_wild = needle.contains('*') || needle.contains('?');
123        if !is_wild {
124            if let Some(pos) = hay[start..].find(&needle) {
125                return Ok(LiteralValue::Int((start + pos + 1) as i64));
126            } else {
127                return Ok(LiteralValue::Error(ExcelError::new_value()));
128            }
129        }
130        // Wildcard scan
131        for offset in start..=hay.len() {
132            if wildcard_match(&needle, &hay[offset..]) {
133                return Ok(LiteralValue::Int((offset + 1) as i64));
134            }
135        }
136        Ok(LiteralValue::Error(ExcelError::from_error_string(
137            "#VALUE!",
138        )))
139    }
140}
141
142fn wildcard_match(pat: &str, text: &str) -> bool {
143    fn rec(p: &[u8], t: &[u8]) -> bool {
144        if p.is_empty() {
145            return true;
146        }
147        match p[0] {
148            b'*' => {
149                for i in 0..=t.len() {
150                    if rec(&p[1..], &t[i..]) {
151                        return true;
152                    }
153                }
154                false
155            }
156            b'?' => {
157                if t.is_empty() {
158                    false
159                } else {
160                    rec(&p[1..], &t[1..])
161                }
162            }
163            c => {
164                if !t.is_empty() && t[0] == c {
165                    rec(&p[1..], &t[1..])
166                } else {
167                    false
168                }
169            }
170        }
171    }
172    rec(pat.as_bytes(), text.as_bytes())
173}
174
175// EXACT(text1,text2)
176#[derive(Debug)]
177pub struct ExactFn;
178impl Function for ExactFn {
179    func_caps!(PURE);
180    fn name(&self) -> &'static str {
181        "EXACT"
182    }
183    fn min_args(&self) -> usize {
184        2
185    }
186    fn arg_schema(&self) -> &'static [ArgSchema] {
187        &ARG_ANY_ONE[..]
188    }
189    fn eval_scalar<'a, 'b>(
190        &self,
191        args: &'a [ArgumentHandle<'a, 'b>],
192        _: &dyn FunctionContext,
193    ) -> Result<LiteralValue, ExcelError> {
194        let a = to_text(&args[0])?;
195        let b = to_text(&args[1])?;
196        Ok(LiteralValue::Boolean(a == b))
197    }
198}
199
200fn number_like<'a, 'b>(a: &ArgumentHandle<'a, 'b>) -> Result<i64, ExcelError> {
201    let v = a.value()?;
202    Ok(match v.as_ref() {
203        LiteralValue::Int(i) => *i,
204        LiteralValue::Number(f) => *f as i64,
205        LiteralValue::Text(t) => t.parse::<i64>().unwrap_or(0),
206        LiteralValue::Boolean(b) => {
207            if *b {
208                1
209            } else {
210                0
211            }
212        }
213        LiteralValue::Empty => 0,
214        LiteralValue::Error(e) => return Err(e.clone()),
215        other => other.to_string().parse::<i64>().unwrap_or(0),
216    })
217}
218
219pub fn register_builtins() {
220    use std::sync::Arc;
221    crate::function_registry::register_function(Arc::new(FindFn));
222    crate::function_registry::register_function(Arc::new(SearchFn));
223    crate::function_registry::register_function(Arc::new(ExactFn));
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use crate::test_workbook::TestWorkbook;
230    use crate::traits::ArgumentHandle;
231    use formualizer_common::LiteralValue;
232    use formualizer_parse::parser::{ASTNode, ASTNodeType};
233    fn lit(v: LiteralValue) -> ASTNode {
234        ASTNode::new(ASTNodeType::Literal(v), None)
235    }
236    #[test]
237    fn find_search() {
238        let wb = TestWorkbook::new()
239            .with_function(std::sync::Arc::new(FindFn))
240            .with_function(std::sync::Arc::new(SearchFn));
241        let ctx = wb.interpreter();
242        let f = ctx.context.get_function("", "FIND").unwrap();
243        let s = ctx.context.get_function("", "SEARCH").unwrap();
244        let hay = lit(LiteralValue::Text("Hello World".into()));
245        let needle = lit(LiteralValue::Text("World".into()));
246        assert_eq!(
247            f.dispatch(
248                &[
249                    ArgumentHandle::new(&needle, &ctx),
250                    ArgumentHandle::new(&hay, &ctx)
251                ],
252                &ctx.function_context(None)
253            )
254            .unwrap(),
255            LiteralValue::Int(7)
256        );
257        let needle2 = lit(LiteralValue::Text("world".into()));
258        assert_eq!(
259            s.dispatch(
260                &[
261                    ArgumentHandle::new(&needle2, &ctx),
262                    ArgumentHandle::new(&hay, &ctx)
263                ],
264                &ctx.function_context(None)
265            )
266            .unwrap(),
267            LiteralValue::Int(7)
268        );
269    }
270}