Skip to main content

doge_runtime/stdlib/
strings.rs

1use crate::error::{DogeError, DogeResult};
2use crate::value::Value;
3
4/// A Str argument as `&str`, or a catchable type error naming the `strings`
5/// member. Thin wrapper over the shared [`crate::stdlib::str_arg`].
6fn str_arg<'a>(fname: &str, v: &'a Value) -> DogeResult<&'a str> {
7    crate::stdlib::str_arg("strings", fname, v)
8}
9
10/// `strings.beeg(s)` — every letter uppercased.
11pub fn strings_beeg(s: &Value) -> DogeResult {
12    Ok(Value::str(str_arg("beeg", s)?.to_uppercase()))
13}
14
15/// `strings.smoll(s)` — every letter lowercased.
16pub fn strings_smoll(s: &Value) -> DogeResult {
17    Ok(Value::str(str_arg("smoll", s)?.to_lowercase()))
18}
19
20/// `strings.trim(s)` — leading and trailing whitespace removed.
21pub fn strings_trim(s: &Value) -> DogeResult {
22    Ok(Value::str(str_arg("trim", s)?.trim()))
23}
24
25/// `strings.split(s, sep)` — a List of the pieces of `s` between each `sep`.
26/// Empty pieces are kept (`"a,,b"` splits into three); splitting on an empty
27/// separator is a catchable ValueError.
28pub fn strings_split(s: &Value, sep: &Value) -> DogeResult {
29    let s = str_arg("split", s)?;
30    let sep = str_arg("split", sep)?;
31    if sep.is_empty() {
32        return Err(DogeError::value_error("cannot split on an empty Str"));
33    }
34    Ok(Value::list(s.split(sep).map(Value::str).collect()))
35}
36
37/// `strings.join(parts, sep)` — the Strs in `parts` joined with `sep`. Every
38/// element of `parts` must be a Str.
39pub fn strings_join(parts: &Value, sep: &Value) -> DogeResult {
40    let items = match parts {
41        Value::List(items) => items.borrow(),
42        _ => return Err(DogeError::type_error("strings.join needs a List of Str")),
43    };
44    let mut pieces = Vec::with_capacity(items.len());
45    for item in items.iter() {
46        match item {
47            Value::Str(piece) => pieces.push(piece.to_string()),
48            _ => return Err(DogeError::type_error("strings.join needs a List of Str")),
49        }
50    }
51    let sep = str_arg("join", sep)?;
52    Ok(Value::str(pieces.join(sep)))
53}
54
55/// `strings.contains(s, needle)` — whether `needle` occurs anywhere in `s`.
56pub fn strings_contains(s: &Value, needle: &Value) -> DogeResult {
57    let s = str_arg("contains", s)?;
58    let needle = str_arg("contains", needle)?;
59    Ok(Value::Bool(s.contains(needle)))
60}
61
62/// `strings.index(s, sub)` — the character offset of the first `sub` in `s`, or
63/// -1 when absent. Char-based like every `Str` operation: `str::find` returns a
64/// byte position, so it is converted to a character count.
65pub fn strings_index(s: &Value, sub: &Value) -> DogeResult {
66    let s = str_arg("index", s)?;
67    let sub = str_arg("index", sub)?;
68    let offset = match s.find(sub) {
69        Some(byte_pos) => s[..byte_pos].chars().count() as i64,
70        None => -1,
71    };
72    Ok(Value::int(offset))
73}
74
75/// `strings.replace(s, from, to)` — every occurrence of `from` in `s` swapped
76/// for `to`. Replacing an empty `from` is a catchable ValueError.
77pub fn strings_replace(s: &Value, from: &Value, to: &Value) -> DogeResult {
78    let s = str_arg("replace", s)?;
79    let from = str_arg("replace", from)?;
80    let to = str_arg("replace", to)?;
81    if from.is_empty() {
82        return Err(DogeError::value_error("cannot replace an empty Str"));
83    }
84    Ok(Value::str(s.replace(from, to)))
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::error::ErrorKind;
91
92    #[test]
93    fn case_and_trim() {
94        assert!(matches!(strings_beeg(&Value::str("wow")).unwrap(), Value::Str(s) if &*s == "WOW"));
95        assert!(
96            matches!(strings_smoll(&Value::str("WOW")).unwrap(), Value::Str(s) if &*s == "wow")
97        );
98        assert!(
99            matches!(strings_trim(&Value::str("  hi  ")).unwrap(), Value::Str(s) if &*s == "hi")
100        );
101    }
102
103    #[test]
104    fn split_keeps_empty_pieces_and_rejects_empty_sep() {
105        let parts = strings_split(&Value::str("a,,b"), &Value::str(",")).unwrap();
106        match parts {
107            Value::List(items) => assert_eq!(items.borrow().len(), 3),
108            _ => panic!("expected a list"),
109        }
110        assert_eq!(
111            strings_split(&Value::str("a"), &Value::str(""))
112                .unwrap_err()
113                .kind,
114            ErrorKind::ValueError
115        );
116    }
117
118    #[test]
119    fn join_needs_all_strs() {
120        let parts = Value::list(vec![Value::str("much"), Value::str("wow")]);
121        assert!(
122            matches!(strings_join(&parts, &Value::str(" ")).unwrap(), Value::Str(s) if &*s == "much wow")
123        );
124        let mixed = Value::list(vec![Value::str("a"), Value::int(1)]);
125        assert_eq!(
126            strings_join(&mixed, &Value::str(" ")).unwrap_err().kind,
127            ErrorKind::TypeError
128        );
129    }
130
131    #[test]
132    fn contains_and_replace() {
133        assert!(matches!(
134            strings_contains(&Value::str("kabosu"), &Value::str("bos")).unwrap(),
135            Value::Bool(true)
136        ));
137        assert!(
138            matches!(strings_replace(&Value::str("a-b-c"), &Value::str("-"), &Value::str("_")).unwrap(), Value::Str(s) if &*s == "a_b_c")
139        );
140        assert_eq!(
141            strings_replace(&Value::str("x"), &Value::str(""), &Value::str("y"))
142                .unwrap_err()
143                .kind,
144            ErrorKind::ValueError
145        );
146    }
147
148    #[test]
149    fn index_returns_char_offset_or_minus_one() {
150        use bigdecimal::ToPrimitive;
151        let at = |s, sub| match strings_index(&Value::str(s), &Value::str(sub)).unwrap() {
152            Value::Int(n) => n.to_i64().unwrap(),
153            _ => panic!("expected an Int"),
154        };
155        assert_eq!(at("kabosu", "bos"), 2);
156        assert_eq!(at("kabosu", "zzz"), -1);
157        // Char-based, not byte-based: the accented char is two bytes but one char.
158        assert_eq!(at("héllo", "llo"), 2);
159    }
160
161    #[test]
162    fn non_str_subject_is_a_type_error() {
163        assert_eq!(
164            strings_trim(&Value::int(1)).unwrap_err().kind,
165            ErrorKind::TypeError
166        );
167    }
168}