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, 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#[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#[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 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 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#[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}