1use std::ops::Range;
2
3use crate::compiler::prelude::*;
4use std::sync::LazyLock;
5
6static DEFAULT_END: LazyLock<Value> = LazyLock::new(|| Value::Bytes(Bytes::from("String length")));
7
8static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
9 vec![
10 Parameter::required(
11 "value",
12 kind::BYTES | kind::ARRAY,
13 "The string or array to slice.",
14 ),
15 Parameter::required(
16 "start",
17 kind::INTEGER,
18 "The inclusive start position. A zero-based index that can be negative.",
19 ),
20 Parameter::optional(
21 "end",
22 kind::INTEGER,
23 "The exclusive end position. A zero-based index that can be negative.",
24 )
25 .default(&DEFAULT_END),
26 ]
27});
28
29#[allow(clippy::cast_possible_wrap)]
30#[allow(clippy::cast_sign_loss)]
31#[allow(clippy::cast_possible_truncation)] fn slice(start: i64, end: Option<i64>, value: Value) -> Resolved {
33 let range = |len: i64| -> ExpressionResult<Range<usize>> {
34 let start = match start {
35 start if start < 0 => start + len,
36 start => start,
37 };
38
39 let end = match end {
40 Some(end) if end < 0 => end + len,
41 Some(end) => end,
42 None => len,
43 };
44
45 match () {
46 () if start < 0 || start > len => {
47 Err(format!(r#""start" must be between "{}" and "{len}""#, -len).into())
48 }
49 () if end < start => Err(r#""end" must be greater or equal to "start""#.into()),
50 () if end > len => Ok(start as usize..len as usize),
51 () => Ok(start as usize..end as usize),
52 }
53 };
54 match value {
55 Value::Bytes(v) => range(v.len() as i64)
56 .map(|range| v.slice(range))
57 .map(Value::from),
58 Value::Array(mut v) => range(v.len() as i64)
59 .map(|range| v.drain(range).collect::<Vec<_>>())
60 .map(Value::from),
61 value => Err(ValueError::Expected {
62 got: value.kind(),
63 expected: Kind::bytes() | Kind::array(Collection::any()),
64 }
65 .into()),
66 }
67}
68
69#[derive(Clone, Copy, Debug)]
70pub struct Slice;
71
72impl Function for Slice {
73 fn identifier(&self) -> &'static str {
74 "slice"
75 }
76
77 fn usage(&self) -> &'static str {
78 indoc! {"
79 Returns a slice of `value` between the `start` and `end` positions.
80
81 If the `start` and `end` parameters are negative, they refer to positions counting from the right of the
82 string or array. If `end` refers to a position that is greater than the length of the string or array,
83 a slice up to the end of the string or array is returned.
84 "}
85 }
86
87 fn category(&self) -> &'static str {
88 Category::String.as_ref()
89 }
90
91 fn return_kind(&self) -> u16 {
92 kind::ARRAY | kind::BYTES
93 }
94
95 fn parameters(&self) -> &'static [Parameter] {
96 PARAMETERS.as_slice()
97 }
98
99 fn examples(&self) -> &'static [Example] {
100 &[
101 example! {
102 title: "Slice a string (positive index)",
103 source: r#"slice!("Supercalifragilisticexpialidocious", start: 5, end: 13)"#,
104 result: Ok("califrag"),
105 },
106 example! {
107 title: "Slice a string (negative index)",
108 source: r#"slice!("Supercalifragilisticexpialidocious", start: 5, end: -14)"#,
109 result: Ok("califragilistic"),
110 },
111 example! {
112 title: "String start",
113 source: r#"slice!("foobar", 3)"#,
114 result: Ok("bar"),
115 },
116 example! {
117 title: "Array start",
118 source: "slice!([0, 1, 2], 1)",
119 result: Ok("[1, 2]"),
120 },
121 ]
122 }
123
124 fn compile(
125 &self,
126 _state: &state::TypeState,
127 _ctx: &mut FunctionCompileContext,
128 arguments: ArgumentList,
129 ) -> Compiled {
130 let value = arguments.required("value");
131 let start = arguments.required("start");
132 let end = arguments.optional("end");
133
134 Ok(SliceFn { value, start, end }.as_expr())
135 }
136}
137
138#[derive(Debug, Clone)]
139struct SliceFn {
140 value: Box<dyn Expression>,
141 start: Box<dyn Expression>,
142 end: Option<Box<dyn Expression>>,
143}
144
145impl FunctionExpression for SliceFn {
146 fn resolve(&self, ctx: &mut Context) -> Resolved {
147 let start = self.start.resolve(ctx)?.try_integer()?;
148 let end = match &self.end {
149 Some(expr) => Some(expr.resolve(ctx)?.try_integer()?),
150 None => None,
151 };
152 let value = self.value.resolve(ctx)?;
153
154 slice(start, end, value)
155 }
156
157 fn type_def(&self, state: &state::TypeState) -> TypeDef {
158 let td = TypeDef::from(Kind::never()).fallible();
159
160 match self.value.type_def(state) {
161 v if v.is_bytes() => td.union(v),
162 v if v.is_array() => td.union(v),
163 _ => td.or_bytes().or_array(Collection::any()),
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use crate::value;
171 use std::collections::BTreeMap;
172
173 use super::*;
174
175 test_function![
176 slice => Slice;
177
178 bytes_0 {
179 args: func_args![value: "foo",
180 start: 0
181 ],
182 want: Ok("foo"),
183 tdef: TypeDef::bytes().fallible(),
184 }
185
186 bytes_1 {
187 args: func_args![value: "foo",
188 start: 1
189 ],
190 want: Ok("oo"),
191 tdef: TypeDef::bytes().fallible(),
192 }
193
194 bytes_2 {
195 args: func_args![value: "foo",
196 start: 2
197 ],
198 want: Ok("o"),
199 tdef: TypeDef::bytes().fallible(),
200 }
201
202 bytes_minus_2 {
203 args: func_args![value: "foo",
204 start: -2
205 ],
206 want: Ok("oo"),
207 tdef: TypeDef::bytes().fallible(),
208 }
209
210 bytes_empty {
211 args: func_args![value: "foo",
212 start: 3
213 ],
214 want: Ok(""),
215 tdef: TypeDef::bytes().fallible(),
216 }
217
218 bytes_empty_start_end {
219 args: func_args![value: "foo",
220 start: 2,
221 end: 2
222 ],
223 want: Ok(""),
224 tdef: TypeDef::bytes().fallible(),
225 }
226
227 bytes_overrun {
228 args: func_args![value: "foo",
229 start: 0,
230 end: 4
231 ],
232 want: Ok("foo"),
233 tdef: TypeDef::bytes().fallible(),
234 }
235
236 bytes_start_overrun {
237 args: func_args![value: "foo",
238 start: 1,
239 end: 5
240 ],
241 want: Ok("oo"),
242 tdef: TypeDef::bytes().fallible(),
243 }
244
245 bytes_negative {
246 args: func_args![value: "Supercalifragilisticexpialidocious",
247 start: -7
248 ],
249 want: Ok("docious"),
250 tdef: TypeDef::bytes().fallible(),
251 }
252
253 bytes_middle {
254 args: func_args![value: "Supercalifragilisticexpialidocious",
255 start: 5,
256 end: 9
257 ],
258 want: Ok("cali"),
259 tdef: TypeDef::bytes().fallible(),
260 }
261
262 array_0 {
263 args: func_args![value: vec![0, 1, 2],
264 start: 0
265 ],
266 want: Ok(vec![0, 1, 2]),
267 tdef: TypeDef::array(Collection::from_parts(BTreeMap::from([
268 (Index::from(0), Kind::integer()),
269 (Index::from(1), Kind::integer()),
270 (Index::from(2), Kind::integer()),
271 ]), Kind::undefined())).fallible(),
272 }
273
274 array_1 {
275 args: func_args![value: vec![0, 1, 2],
276 start: 1
277 ],
278 want: Ok(vec![1, 2]),
279 tdef: TypeDef::array(Collection::from_parts(BTreeMap::from([
280 (Index::from(0), Kind::integer()),
281 (Index::from(1), Kind::integer()),
282 (Index::from(2), Kind::integer()),
283 ]), Kind::undefined())).fallible(),
284 }
285
286 array_minus_2 {
287 args: func_args![value: vec![0, 1, 2],
288 start: -2
289 ],
290 want: Ok(vec![1, 2]),
291 tdef: TypeDef::array(Collection::from_parts(BTreeMap::from([
292 (Index::from(0), Kind::integer()),
293 (Index::from(1), Kind::integer()),
294 (Index::from(2), Kind::integer()),
295 ]), Kind::undefined())).fallible(),
296 }
297
298 array_mixed_types {
299 args: func_args![value: value!([0, "ook", true]),
300 start: 1
301 ],
302 want: Ok(value!(["ook", true])),
303 tdef: TypeDef::array(Collection::from_parts(BTreeMap::from([
304 (Index::from(0), Kind::integer()),
305 (Index::from(1), Kind::bytes()),
306 (Index::from(2), Kind::boolean()),
307 ]), Kind::undefined())).fallible(),
308 }
309
310 error_after_end {
311 args: func_args![value: "foo",
312 start: 4
313 ],
314 want: Err(r#""start" must be between "-3" and "3""#),
315 tdef: TypeDef::bytes().fallible(),
316 }
317
318 error_minus_before_start {
319 args: func_args![value: "foo",
320 start: -4
321 ],
322 want: Err(r#""start" must be between "-3" and "3""#),
323 tdef: TypeDef::bytes().fallible(),
324 }
325
326 error_start_end {
327 args: func_args![value: "foo",
328 start: 2,
329 end: 1
330 ],
331 want: Err(r#""end" must be greater or equal to "start""#),
332 tdef: TypeDef::bytes().fallible(),
333 }
334 ];
335}