1use crate::compiler::prelude::*;
2use std::sync::LazyLock;
3
4static DEFAULT_FROM: LazyLock<Value> = LazyLock::new(|| Value::Integer(0));
5
6static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
7 vec![
8 Parameter::required("value", kind::BYTES, "The string to find the pattern in."),
9 Parameter::required(
10 "pattern",
11 kind::BYTES | kind::REGEX,
12 "The regular expression or string pattern to match against.",
13 ),
14 Parameter::optional("from", kind::INTEGER, "Offset to start searching.")
15 .default(&DEFAULT_FROM),
16 ]
17});
18
19#[allow(clippy::cast_possible_wrap)]
20fn find(value: Value, pattern: Value, from: Value) -> Resolved {
21 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
23 let from = from.try_integer()? as usize;
24
25 Ok(FindFn::find(value, pattern, from)?
26 .map_or(Value::Null, |value| Value::Integer(value as i64)))
27}
28
29#[derive(Clone, Copy, Debug)]
30pub struct Find;
31
32impl Function for Find {
33 fn identifier(&self) -> &'static str {
34 "find"
35 }
36
37 fn usage(&self) -> &'static str {
38 "Determines from left to right the start position of the first found element in `value` that matches `pattern`. Returns `-1` if not found."
39 }
40
41 fn category(&self) -> &'static str {
42 Category::String.as_ref()
43 }
44
45 fn return_kind(&self) -> u16 {
46 kind::INTEGER
47 }
48
49 fn parameters(&self) -> &'static [Parameter] {
50 PARAMETERS.as_slice()
51 }
52
53 fn examples(&self) -> &'static [Example] {
54 &[
55 example! {
56 title: "Match text",
57 source: r#"find("foobar", "bar")"#,
58 result: Ok("3"),
59 },
60 example! {
61 title: "Match text at start",
62 source: r#"find("foobar", "foo")"#,
63 result: Ok("0"),
64 },
65 example! {
66 title: "Match regex",
67 source: r#"find("foobar", r'b.r')"#,
68 result: Ok("3"),
69 },
70 example! {
71 title: "No matches",
72 source: r#"find("foobar", "baz")"#,
73 result: Ok("null"),
74 },
75 example! {
76 title: "With an offset",
77 source: r#"find("foobarfoobarfoo", "bar", 4)"#,
78 result: Ok("9"),
79 },
80 ]
81 }
82
83 fn compile(
84 &self,
85 _state: &state::TypeState,
86 _ctx: &mut FunctionCompileContext,
87 arguments: ArgumentList,
88 ) -> Compiled {
89 let value = arguments.required("value");
90 let pattern = arguments.required("pattern");
91 let from = arguments.optional("from");
92
93 Ok(FindFn {
94 value,
95 pattern,
96 from,
97 }
98 .as_expr())
99 }
100}
101
102#[derive(Debug, Clone)]
103struct FindFn {
104 value: Box<dyn Expression>,
105 pattern: Box<dyn Expression>,
106 from: Option<Box<dyn Expression>>,
107}
108
109impl FindFn {
110 fn find_regex_in_str(value: &str, regex: &ValueRegex, offset: usize) -> Option<usize> {
111 regex.find_at(value, offset).map(|found| found.start())
112 }
113
114 fn find_bytes_in_bytes(value: &Bytes, pattern: &Bytes, offset: usize) -> Option<usize> {
115 if pattern.len() > value.len() {
116 return None;
117 }
118 for from in offset..=(value.len() - pattern.len()) {
119 let to = from + pattern.len();
120 if value[from..to] == *pattern {
121 return Some(from);
122 }
123 }
124 None
125 }
126
127 fn find(value: Value, pattern: Value, offset: usize) -> ExpressionResult<Option<usize>> {
128 match pattern {
129 Value::Bytes(bytes) => Ok(Self::find_bytes_in_bytes(
130 &value.try_bytes()?,
131 &bytes,
132 offset,
133 )),
134 Value::Regex(regex) => Ok(Self::find_regex_in_str(
135 &value.try_bytes_utf8_lossy()?,
136 ®ex,
137 offset,
138 )),
139 other => Err(ValueError::Expected {
140 got: other.kind(),
141 expected: Kind::bytes() | Kind::regex(),
142 }
143 .into()),
144 }
145 }
146}
147
148impl FunctionExpression for FindFn {
149 fn resolve(&self, ctx: &mut Context) -> Resolved {
150 let value = self.value.resolve(ctx)?;
151 let pattern = self.pattern.resolve(ctx)?;
152 let from = self
153 .from
154 .map_resolve_with_default(ctx, || DEFAULT_FROM.clone())?;
155
156 find(value, pattern, from)
157 }
158
159 fn type_def(&self, _: &state::TypeState) -> TypeDef {
160 TypeDef::integer().infallible()
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use regex::Regex;
167
168 use crate::value;
169
170 use super::*;
171
172 test_function![
173 find => Find;
174
175 str_matching_end {
176 args: func_args![value: "foobar", pattern: "bar"],
177 want: Ok(value!(3)),
178 tdef: TypeDef::integer().infallible(),
179 }
180
181 str_matching_beginning {
182 args: func_args![value: "foobar", pattern: "foo"],
183 want: Ok(value!(0)),
184 tdef: TypeDef::integer().infallible(),
185 }
186
187 str_matching_middle {
188 args: func_args![value: "foobar", pattern: "ob"],
189 want: Ok(value!(2)),
190 tdef: TypeDef::integer().infallible(),
191 }
192
193 str_too_long {
194 args: func_args![value: "foo", pattern: "foobar"],
195 want: Ok(value!(null)),
196 tdef: TypeDef::integer().infallible(),
197 }
198
199 regex_matching_end {
200 args: func_args![value: "foobar", pattern: Value::Regex(Regex::new("bar").unwrap().into())],
201 want: Ok(value!(3)),
202 tdef: TypeDef::integer().infallible(),
203 }
204
205 regex_matching_start {
206 args: func_args![value: "foobar", pattern: Value::Regex(Regex::new("fo+z?").unwrap().into())],
207 want: Ok(value!(0)),
208 tdef: TypeDef::integer().infallible(),
209 }
210
211 wrong_pattern {
212 args: func_args![value: "foobar", pattern: Value::Integer(42)],
213 want: Err("expected string or regex, got integer"),
214 tdef: TypeDef::integer().infallible(),
215 }
216 ];
217}