1use crate::compiler::prelude::*;
2use std::sync::LazyLock;
3
4static DEFAULT_CASE_SENSITIVE: LazyLock<Value> = LazyLock::new(|| Value::Boolean(true));
5
6static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
7 vec![
8 Parameter::required("value", kind::BYTES, "The string to search."),
9 Parameter::required(
10 "substring",
11 kind::BYTES,
12 "The substring that the `value` must start with.",
13 ),
14 Parameter::optional(
15 "case_sensitive",
16 kind::BOOLEAN,
17 "Whether the match should be case sensitive.",
18 )
19 .default(&DEFAULT_CASE_SENSITIVE),
20 ]
21});
22
23struct Chars<'a> {
24 bytes: &'a Bytes,
25 pos: usize,
26}
27
28impl<'a> Chars<'a> {
29 fn new(bytes: &'a Bytes) -> Self {
30 Self { bytes, pos: 0 }
31 }
32}
33
34impl Iterator for Chars<'_> {
35 type Item = std::result::Result<char, u8>;
36
37 fn next(&mut self) -> Option<Self::Item> {
38 if self.pos >= self.bytes.len() {
39 return None;
40 }
41
42 let width = utf8_width::get_width(self.bytes[self.pos]);
43 if width == 1 {
44 self.pos += 1;
45 Some(Ok(self.bytes[self.pos - 1] as char))
46 } else {
47 let c = std::str::from_utf8(&self.bytes[self.pos..self.pos + width]);
48 if let Ok(chr) = c {
49 self.pos += width;
50 Some(Ok(chr.chars().next().unwrap()))
51 } else {
52 self.pos += 1;
53 Some(Err(self.bytes[self.pos]))
54 }
55 }
56 }
57}
58
59#[derive(Clone, Copy)]
60enum Case {
61 Sensitive,
62 Insensitive,
63}
64
65fn starts_with(bytes: &Bytes, starts: &Bytes, case: Case) -> bool {
66 if bytes.len() < starts.len() {
67 return false;
68 }
69
70 match case {
71 Case::Sensitive => starts[..] == bytes[0..starts.len()],
72 Case::Insensitive => Chars::new(starts)
73 .zip(Chars::new(bytes))
74 .all(|(a, b)| match (a, b) {
75 (Ok(a), Ok(b)) => {
76 if a.is_ascii() && b.is_ascii() {
77 a.eq_ignore_ascii_case(&b)
78 } else {
79 a.to_lowercase().zip(b.to_lowercase()).all(|(a, b)| a == b)
80 }
81 }
82 _ => false,
83 }),
84 }
85}
86
87#[derive(Clone, Copy, Debug)]
88pub struct StartsWith;
89
90impl Function for StartsWith {
91 fn identifier(&self) -> &'static str {
92 "starts_with"
93 }
94
95 fn usage(&self) -> &'static str {
96 "Determines whether `value` begins with `substring`."
97 }
98
99 fn category(&self) -> &'static str {
100 Category::String.as_ref()
101 }
102
103 fn return_kind(&self) -> u16 {
104 kind::BOOLEAN
105 }
106
107 fn parameters(&self) -> &'static [Parameter] {
108 PARAMETERS.as_slice()
109 }
110
111 fn examples(&self) -> &'static [Example] {
112 &[
113 example! {
114 title: "String starts with (case sensitive)",
115 source: r#"starts_with("The Needle In The Haystack", "The Needle")"#,
116 result: Ok("true"),
117 },
118 example! {
119 title: "String starts with (case insensitive)",
120 source: r#"starts_with("The Needle In The Haystack", "the needle", case_sensitive: false)"#,
121 result: Ok("true"),
122 },
123 example! {
124 title: "String starts with (case sensitive failure)",
125 source: r#"starts_with("foobar", "F")"#,
126 result: Ok("false"),
127 },
128 ]
129 }
130
131 fn compile(
132 &self,
133 _state: &state::TypeState,
134 _ctx: &mut FunctionCompileContext,
135 arguments: ArgumentList,
136 ) -> Compiled {
137 let value = arguments.required("value");
138 let substring = arguments.required("substring");
139 let case_sensitive = arguments.optional("case_sensitive");
140
141 Ok(StartsWithFn {
142 value,
143 substring,
144 case_sensitive,
145 }
146 .as_expr())
147 }
148}
149
150#[derive(Debug, Clone)]
151struct StartsWithFn {
152 value: Box<dyn Expression>,
153 substring: Box<dyn Expression>,
154 case_sensitive: Option<Box<dyn Expression>>,
155}
156
157impl FunctionExpression for StartsWithFn {
158 fn resolve(&self, ctx: &mut Context) -> Resolved {
159 let case_sensitive = self
160 .case_sensitive
161 .map_resolve_with_default(ctx, || DEFAULT_CASE_SENSITIVE.clone())?
162 .try_boolean()?;
163 let case_sensitive = if case_sensitive {
164 Case::Sensitive
165 } else {
166 Case::Insensitive
167 };
168
169 let substring = self.substring.resolve(ctx)?;
170 let substring = substring.try_bytes()?;
171
172 let value = self.value.resolve(ctx)?;
173 let value = value.try_bytes()?;
174
175 Ok(starts_with(&value, &substring, case_sensitive).into())
176 }
177
178 fn type_def(&self, _: &state::TypeState) -> TypeDef {
179 TypeDef::boolean().infallible()
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 test_function![
188 starts_with => StartsWith;
189
190 no {
191 args: func_args![value: "foo",
192 substring: "bar"
193 ],
194 want: Ok(false),
195 tdef: TypeDef::boolean().infallible(),
196 }
197
198 subset {
199 args: func_args![value: "foo",
200 substring: "foobar"
201 ],
202 want: Ok(false),
203 tdef: TypeDef::boolean().infallible(),
204 }
205
206 total {
207 args: func_args![value: "foo",
208 substring: "foo"
209 ],
210 want: Ok(true),
211 tdef: TypeDef::boolean().infallible(),
212 }
213
214 middle {
215 args: func_args![value: "foobar",
216 substring: "oba"
217 ],
218 want: Ok(false),
219 tdef: TypeDef::boolean().infallible(),
220 }
221
222 start {
223 args: func_args![value: "foobar",
224 substring: "foo"
225 ],
226 want: Ok(true),
227 tdef: TypeDef::boolean().infallible(),
228 }
229
230 end {
231 args: func_args![value: "foobar",
232 substring: "bar"
233 ],
234 want: Ok(false),
235 tdef: TypeDef::boolean().infallible(),
236 }
237
238
239 case_sensitive_same_case {
240 args: func_args![value: "FOObar",
241 substring: "FOO"
242 ],
243 want: Ok(true),
244 tdef: TypeDef::boolean().infallible(),
245 }
246
247 case_sensitive_different_case {
248 args: func_args![value: "foobar",
249 substring: "FOO"
250 ],
251 want: Ok(false),
252 tdef: TypeDef::boolean().infallible(),
253 }
254
255 case_insensitive_different_case {
256 args: func_args![value: "foobar",
257 substring: "FOO",
258 case_sensitive: false
259 ],
260 want: Ok(true),
261 tdef: TypeDef::boolean().infallible(),
262 }
263
264 unicode_same_case {
265 args: func_args![value: "𛋙ၺ㚺𛋙Zonkکᤊᰙ𛋙Ꮺ믚㋫𐠘𒃪𖾛𞺘ᰙꢝⶺ觨⨙ઉzook",
266 substring: "𛋙ၺ㚺𛋙Zonkکᤊᰙ𛋙",
267 case_sensitive: true
268 ],
269 want: Ok(true),
270 tdef: TypeDef::boolean().infallible(),
271 }
272
273 unicode_sensitive_different_case {
274 args: func_args![value: "ξ𛋙ၺ㚺𛋙Zonkکᤊᰙ𛋙Ꮺ믚㋫𐠘𒃪𖾛𞺘ᰙꢝⶺ觨⨙ઉzook",
275 substring: "Ξ𛋙ၺ㚺𛋙Zonkکᤊᰙ𛋙",
276 case_sensitive: true
277 ],
278 want: Ok(false),
279 tdef: TypeDef::boolean().infallible(),
280 }
281
282 unicode_insensitive_different_case {
283 args: func_args![value: "ξ𛋙ၺ㚺𛋙Zonkکᤊᰙ𛋙Ꮺ믚㋫𐠘𒃪𖾛𞺘ᰙꢝⶺ觨⨙ઉzook",
284 substring: "Ξ𛋙ၺ㚺𛋙Zonkکᤊᰙ𛋙",
285 case_sensitive: false
286 ],
287 want: Ok(true),
288 tdef: TypeDef::boolean().infallible(),
289 }
290 ];
291}