slac/stdlib/
string.rs

1//! Functions to manipulate [`Value::String`] variables.
2
3use crate::{
4    Value,
5    function::{Arity, Function},
6};
7
8use super::error::{NativeError, NativeResult};
9
10/// Returns all string functions as a fixed size array.
11#[rustfmt::skip]
12pub fn functions() -> Vec<Function> {
13    vec![
14        Function::new(chr, Arity::required(1), "chr(ord: Number): String"),
15        Function::new(ord, Arity::required(1), "ord(char: String): Number"),
16        Function::new(lowercase, Arity::required(1), "lowercase(text: String): String"),
17        Function::new(uppercase, Arity::required(1), "uppercase(text: String): String"),
18        Function::new(same_text, Arity::required(2), "same_text(left: String, right: String): Boolean"),
19        Function::new(split, Arity::required(2), "split(line: String, separator: String): Array<String>"),
20        Function::new(split_csv, Arity::optional(1, 1), "split_csv(line: String, separator: String = ';'): Array<String>"),
21        Function::new(trim, Arity::required(1), "trim(text: String): String"),
22        Function::new(trim_left, Arity::required(1), "trim_left(text: String): String"),
23        Function::new(trim_right, Arity::required(1), "trim_right(text: String): String"),
24    ]
25}
26
27/// Converts a [`Value::Number`] into a [`Value::String`] containing a single ASCII character.
28///
29/// * Declaration: `chr(ord: Number): String`
30///
31/// # Errors
32///
33/// Will return [`NativeError::CustomError`] if the supplied number is outside of ASCII character range.
34/// Will return [`NativeError::WrongParameterCount`] if there is a mismatch in the supplied parameters.
35/// Will return [`NativeError::WrongParameterType`] if the the supplied parameters have the wrong type.
36#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
37pub fn chr(params: &[Value]) -> NativeResult {
38    match params {
39        [Value::Number(ordinal)] if (0.0..127.0).contains(ordinal) => Ok(Value::String(
40            char::from_u32(*ordinal as u32).unwrap_or('\0').to_string(),
41        )),
42        [Value::Number(_)] => Err(NativeError::from("number is out of ASCII range")),
43        [_] => Err(NativeError::WrongParameterType),
44        _ => Err(NativeError::WrongParameterCount(1)),
45    }
46}
47
48/// Converts a single character [`Value::String`] into a [`Value::Number`] containing it's ordinal value.
49///
50/// * Declaration: `ord(char: String): Number`
51///
52/// # Errors
53///
54/// Will return [`NativeError::CustomError`] if the supplied number is outside of ASCII character range.
55/// Will return [`NativeError::WrongParameterCount`] if there is a mismatch in the supplied parameters.
56/// Will return [`NativeError::WrongParameterType`] if the the supplied parameters have the wrong type.
57pub fn ord(params: &[Value]) -> NativeResult {
58    match params {
59        [Value::String(char)] if char.chars().count() == 1 => {
60            if char.is_ascii() {
61                Ok(Value::Number(f64::from(
62                    char.chars().next().unwrap_or('\0') as u8,
63                )))
64            } else {
65                Err(NativeError::from("character is out of ASCII range"))
66            }
67        }
68        [Value::String(_)] => Err(NativeError::from("string is too long")),
69        [_] => Err(NativeError::WrongParameterType),
70        _ => Err(NativeError::WrongParameterCount(1)),
71    }
72}
73
74/// Converts a [`Value::String`] to lowercase.
75///
76/// * Declaration: `lowercase(text: String): String`
77///
78/// # Errors
79///
80/// Will return [`NativeError::WrongParameterCount`] if there is a mismatch in the supplied parameters.
81/// Will return [`NativeError::WrongParameterType`] if the the supplied parameters have the wrong type.
82pub fn lowercase(params: &[Value]) -> NativeResult {
83    match params {
84        [Value::String(text)] => Ok(Value::String(text.to_lowercase())),
85        [_] => Err(NativeError::WrongParameterType),
86        _ => Err(NativeError::WrongParameterCount(1)),
87    }
88}
89
90/// Converts a [`Value::String`] to uppercase.
91///
92/// * Declaration: `uppercase(text: String): String`
93///
94/// # Errors
95///
96/// Will return [`NativeError::WrongParameterCount`] if there is a mismatch in the supplied parameters.
97/// Will return [`NativeError::WrongParameterType`] if the the supplied parameters have the wrong type.
98pub fn uppercase(params: &[Value]) -> NativeResult {
99    match params {
100        [Value::String(text)] => Ok(Value::String(text.to_uppercase())),
101        [_] => Err(NativeError::WrongParameterType),
102        _ => Err(NativeError::WrongParameterCount(1)),
103    }
104}
105
106/// Compares two [`Value::String`] by text content.
107///
108/// * Declaration: `same_text(left: String, right: String): Boolean`
109///
110/// # Remarks
111///
112/// Comparison is made by comparing the lowercase values.
113///
114/// # Errors
115///
116/// Will return [`NativeError::WrongParameterCount`] if there is a mismatch in the supplied parameters.
117/// Will return [`NativeError::WrongParameterType`] if the the supplied parameters have the wrong type.
118pub fn same_text(params: &[Value]) -> NativeResult {
119    match params {
120        [Value::String(left), Value::String(right)] => {
121            Ok(Value::Boolean(left.to_lowercase() == right.to_lowercase()))
122        }
123        [_, _] => Err(NativeError::WrongParameterType),
124        _ => Err(NativeError::WrongParameterCount(2)),
125    }
126}
127
128/// Splits a [`Value::String`] into a [`Value::Array`] according to a separator.
129///
130/// * Declaration: `split(line: String, separator: String): Array<String>`
131///
132/// # Errors
133///
134/// Will return [`NativeError::WrongParameterCount`] if there is a mismatch in the supplied parameters.
135/// Will return [`NativeError::WrongParameterType`] if the the supplied parameters have the wrong type.
136pub fn split(params: &[Value]) -> NativeResult {
137    match params {
138        [Value::String(line), Value::String(separator)] => {
139            let values = line
140                .split(separator)
141                .map(String::from)
142                .map(Value::String)
143                .collect();
144
145            Ok(Value::Array(values))
146        }
147        [_, _] => Err(NativeError::WrongParameterType),
148        _ => Err(NativeError::WrongParameterCount(1)),
149    }
150}
151
152fn char_from_value(value: &Value) -> Option<char> {
153    match value {
154        Value::String(string) if string.len() == 1 => string.chars().next(),
155        _ => None,
156    }
157}
158
159fn parse_csv(line: &str, separator: char) -> Vec<String> {
160    let mut result = Vec::new();
161    let mut field = String::new();
162    let mut in_quotes = false;
163
164    for c in line.chars() {
165        if c == separator && !in_quotes {
166            result.push(field.clone());
167            field.clear();
168        } else if c == '"' {
169            in_quotes = !in_quotes;
170        } else {
171            field.push(c);
172        }
173    }
174
175    result.push(field);
176    result
177}
178
179/// Splits a csv [`Value::String`] into a [`Value::Array`].
180///
181/// * Declaration: `split_csv(line: String, separator: String = ';'): Array<String>`
182///
183/// # Errors
184///
185/// Will return [`NativeError::WrongParameterCount`] if there is a mismatch in the supplied parameters.
186/// Will return [`NativeError::WrongParameterType`] if the the supplied parameters have the wrong type.
187pub fn split_csv(params: &[Value]) -> NativeResult {
188    let separator = params.get(1).and_then(char_from_value).unwrap_or(';');
189
190    match params {
191        [Value::String(line), ..] => {
192            let values = parse_csv(line, separator)
193                .into_iter()
194                .map(Value::String)
195                .collect();
196            Ok(Value::Array(values))
197        }
198        [_, ..] => Err(NativeError::WrongParameterType),
199        _ => Err(NativeError::WrongParameterCount(1)),
200    }
201}
202
203/// Trims the whitespace of a [`Value::String`] on both sides.
204///
205/// * Declaration: `trim(text: String): String`
206///
207/// # Errors
208///
209/// Will return [`NativeError::WrongParameterCount`] if there is a mismatch in the supplied parameters.
210/// Will return [`NativeError::WrongParameterType`] if the the supplied parameters have the wrong type.
211pub fn trim(params: &[Value]) -> NativeResult {
212    match params {
213        [Value::String(text)] => Ok(Value::String(text.trim().to_string())),
214        [_] => Err(NativeError::WrongParameterType),
215        _ => Err(NativeError::WrongParameterCount(1)),
216    }
217}
218
219/// Trims the whitespace of a [`Value::String`] on the left side of the String.
220///
221/// * Declaration: `trim_left(text: String): String`
222///
223/// # Errors
224///
225/// Will return [`NativeError::WrongParameterCount`] if there is a mismatch in the supplied parameters.
226/// Will return [`NativeError::WrongParameterType`] if the the supplied parameters have the wrong type.
227pub fn trim_left(params: &[Value]) -> NativeResult {
228    match params {
229        [Value::String(text)] => Ok(Value::String(text.trim_start().to_string())),
230        [_] => Err(NativeError::WrongParameterType),
231        _ => Err(NativeError::WrongParameterCount(1)),
232    }
233}
234
235/// Trims the whitespace of a [`Value::String`] on the right side of the String.
236///
237/// * Declaration: `trim_right(text: String): String`
238///
239/// # Errors
240///
241/// Will return [`NativeError::WrongParameterCount`] if there is a mismatch in the supplied parameters.
242/// Will return [`NativeError::WrongParameterType`] if the the supplied parameters have the wrong type.
243pub fn trim_right(params: &[Value]) -> NativeResult {
244    match params {
245        [Value::String(text)] => Ok(Value::String(text.trim_end().to_string())),
246        [_] => Err(NativeError::WrongParameterType),
247        _ => Err(NativeError::WrongParameterCount(1)),
248    }
249}
250
251#[cfg(test)]
252mod test {
253    use super::*;
254    use crate::Value;
255
256    #[test]
257    fn string_ord() {
258        assert_eq!(
259            Ok(Value::Number(97.0)),
260            ord(&vec![Value::String(String::from("a"))])
261        );
262
263        assert_eq!(
264            Ok(Value::Number(13.0)),
265            ord(&vec![Value::String(String::from("\r"))])
266        );
267        assert_eq!(
268            Ok(Value::Number(10.0)),
269            ord(&vec![Value::String(String::from("\n"))])
270        );
271
272        assert!(ord(&vec![Value::String(String::from("Hello World"))]).is_err());
273        assert!(ord(&vec![Value::String(String::from("🙄"))]).is_err());
274    }
275
276    #[test]
277    fn string_chr() {
278        assert_eq!(
279            Ok(Value::String(String::from("a"))),
280            chr(&vec![Value::Number(97.0)])
281        );
282
283        assert_eq!(
284            Ok(Value::String(String::from("\0"))),
285            chr(&vec![Value::Number(0.0)])
286        );
287
288        assert!(chr(&vec![Value::Number(256.0)]).is_err());
289    }
290
291    #[test]
292    fn string_lowercase() {
293        assert_eq!(
294            Ok(Value::String(String::from("hello world"))),
295            lowercase(&vec![Value::String(String::from("Hello World"))])
296        );
297
298        assert!(lowercase(&vec![]).is_err());
299        assert!(lowercase(&vec![Value::Boolean(true)]).is_err());
300    }
301
302    #[test]
303    fn string_uppercase() {
304        assert_eq!(
305            Ok(Value::String(String::from("HELLO WORLD"))),
306            uppercase(&vec![Value::String(String::from("Hello World"))])
307        );
308
309        assert!(uppercase(&vec![]).is_err());
310        assert!(uppercase(&vec![Value::Boolean(true)]).is_err());
311    }
312
313    #[test]
314    fn string_split_csv() {
315        assert_eq!(
316            Ok(Value::Array(vec![
317                Value::String(String::from("Hello; World")),
318                Value::String(String::from("1234")),
319                Value::String(String::from("")),
320                Value::String(String::from("End"))
321            ])),
322            split_csv(&vec![Value::String(String::from(
323                "\"Hello; World\";1234;;End"
324            ))])
325        );
326
327        assert_eq!(
328            Ok(Value::Array(vec![Value::String(String::new())])),
329            split_csv(&vec![Value::String(String::from(""))])
330        );
331    }
332
333    #[test]
334    fn string_split() {
335        assert_eq!(
336            Ok(Value::Array(vec![
337                Value::String(String::from("\"Hello")),
338                Value::String(String::from(" World\"")),
339                Value::String(String::from("1234")),
340                Value::String(String::from("")),
341                Value::String(String::from("End"))
342            ])),
343            split(&vec![
344                Value::String(String::from("\"Hello; World\";1234;;End")),
345                Value::String(String::from(";"))
346            ])
347        );
348
349        assert_eq!(
350            Ok(Value::Array(vec![Value::String(String::new())])),
351            split(&vec![
352                Value::String(String::from("")),
353                Value::String(String::from(";"))
354            ])
355        );
356    }
357
358    #[test]
359    fn string_same_text() {
360        assert_eq!(
361            Ok(Value::Boolean(true)),
362            same_text(&vec![
363                Value::String(String::from("hello world")),
364                Value::String(String::from("Hello World"))
365            ])
366        );
367
368        assert_eq!(
369            Ok(Value::Boolean(false)),
370            same_text(&vec![
371                Value::String(String::from("hallo world")),
372                Value::String(String::from("hello world"))
373            ])
374        );
375    }
376
377    #[test]
378    fn string_trim() {
379        assert_eq!(
380            Ok(Value::String(String::from("Hello World"))),
381            trim(&vec![Value::String(String::from("  Hello World       "))])
382        );
383
384        assert!(trim(&vec![]).is_err());
385        assert!(trim(&vec![Value::Boolean(true)]).is_err());
386
387        assert_eq!(
388            Ok(Value::String(String::from("Hello World       "))),
389            trim_left(&vec![Value::String(String::from("  Hello World       "))])
390        );
391
392        assert_eq!(
393            Ok(Value::String(String::from("  Hello World"))),
394            trim_right(&vec![Value::String(String::from("  Hello World       "))])
395        );
396    }
397}