Skip to main content

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