Skip to main content

tancore/
string.rs

1use tan::{
2    context::Context,
3    error::Error,
4    expr::{format_value, Expr},
5    util::{args::unpack_stringable_arg, module_util::require_module},
6};
7
8// #todo string push/append/concat, make similar to array: push one char, append/concat another string, ++ handles both.
9
10// #todo rearrange the functions in some logical order, can be alphabetical.
11
12// #todo (compare str1 str2) ; => Ordering
13// #todo (to-lowercase str) or (lowercased str)
14// #todo (to-uppercase str) or (uppercased str)
15
16// #idea just use the String constructor: (String "hello " num " guys"), or even (Str "hello " num " guys")
17// #todo support: (Str (HTML-Expr (p "This is a nice paragraph!")))
18pub fn string_new(args: &[Expr]) -> Result<Expr, Error> {
19    let output = args.iter().fold(String::new(), |mut str, x| {
20        str.push_str(&format_value(x));
21        str
22    });
23
24    Ok(Expr::String(output))
25}
26
27// #todo better name: `size`?
28// #insight `count` is not a good name for length/len, better to be used as verb
29pub fn string_get_length(args: &[Expr]) -> Result<Expr, Error> {
30    let [this] = args else {
31        return Err(Error::invalid_arguments(
32            "`chars` requires `this` argument",
33            None,
34        ));
35    };
36
37    let Expr::String(s) = this.unpack() else {
38        return Err(Error::invalid_arguments(
39            "`this` argument should be a String",
40            this.range(),
41        ));
42    };
43
44    Ok(Expr::Int(s.len() as i64))
45}
46
47// #todo Implement in Tan?
48pub fn string_is_empty(args: &[Expr]) -> Result<Expr, Error> {
49    let s = unpack_stringable_arg(args, 0, "s")?;
50    Ok(Expr::Bool(s.is_empty()))
51}
52
53// #todo trim-start
54// #todo trim-end
55
56pub fn string_trim(args: &[Expr]) -> Result<Expr, Error> {
57    let [this] = args else {
58        return Err(Error::invalid_arguments("requires `this` argument", None));
59    };
60
61    let Expr::String(s) = this.unpack() else {
62        return Err(Error::invalid_arguments(
63            "`this` argument should be a String",
64            this.range(),
65        ));
66    };
67
68    Ok(Expr::string(s.trim()))
69}
70
71// #todo how to implement a mutating function?
72// #todo return (Maybe Char) or (Maybe Rune), handle case of empty string.
73/// Removes the last character from the string buffer and returns it.
74pub fn string_pop(_args: &[Expr]) -> Result<Expr, Error> {
75    // #todo handle the string mutation!
76    // #todo handle empty string case!!
77
78    todo!()
79}
80
81// #todo enforce range within string length
82// #todo rename to `cut`? (as in 'cut a slice')
83// #todo relation with range?
84// #todo pass range as argument?
85// #todo support negative index: -1 => length - 1
86// #insight negative index _may_ be problematic if the index is computed and returns negative by mistake.
87/// (slice str 2 5)
88/// (slice str 2)
89/// (slice str 2 -2) ; -2 is length - 2
90pub fn string_slice(args: &[Expr]) -> Result<Expr, Error> {
91    let [this, start, ..] = args else {
92        return Err(Error::invalid_arguments(
93            "`slice` requires `this` and start arguments",
94            None,
95        ));
96    };
97
98    let Some(s) = this.as_stringable() else {
99        return Err(Error::invalid_arguments(
100            "`this` argument should be a String",
101            this.range(),
102        ));
103    };
104
105    let Expr::Int(start) = start.unpack() else {
106        return Err(Error::invalid_arguments(
107            "`start` argument should be an Int",
108            this.range(),
109        ));
110    };
111
112    let start = *start;
113
114    let end = if let Some(end) = args.get(2) {
115        let Expr::Int(end) = end.unpack() else {
116            return Err(Error::invalid_arguments(
117                "`end` argument should be an Int",
118                this.range(),
119            ));
120        };
121        *end
122    } else {
123        s.len() as i64
124    };
125
126    let start = start as usize;
127    let end = if end < 0 {
128        // #todo supporting negative index may hide errors if the index is computed
129        // #todo offer a link to only support negative values for constant index
130        // If the end argument is negative it indexes from the end of the string.
131        (s.len() as i64 + end) as usize
132    } else {
133        end as usize
134    };
135
136    let string_slice = &s[start..end];
137
138    Ok(Expr::string(string_slice))
139}
140
141// #todo search `recognize_range`.
142// #todo this should reuse the plain string_slice method.
143/// Cuts a slice out fo a string, defined by a range.
144pub fn string_slice_range(args: &[Expr]) -> Result<Expr, Error> {
145    let [this, start, ..] = args else {
146        return Err(Error::invalid_arguments(
147            "`slice` requires `this` and range arguments",
148            None,
149        ));
150    };
151
152    let Expr::String(s) = this.unpack() else {
153        return Err(Error::invalid_arguments(
154            "`this` argument should be a String",
155            this.range(),
156        ));
157    };
158
159    let Expr::IntRange(start, end, ..) = start.unpack() else {
160        return Err(Error::invalid_arguments(
161            "`range` argument should be a Range",
162            this.range(),
163        ));
164    };
165
166    // #todo support open-ended ranges.
167    // #todo extract the following.
168
169    let start = *start;
170    let end = *end;
171
172    let start = start as usize;
173    let end = if end < 0 {
174        // #todo supporting negative index may hide errors if the index is computed
175        // #todo offer a link to only support negative values for constant index
176        // If the end argument is negative it indexes from the end of the string.
177        (s.len() as i64 + end) as usize
178    } else {
179        end as usize
180    };
181
182    let string_slice = &s[start..end];
183
184    Ok(Expr::string(string_slice))
185}
186
187/// Returns a char iterable for the chars in the string.
188pub fn string_chars(args: &[Expr]) -> Result<Expr, Error> {
189    let [this] = args else {
190        return Err(Error::invalid_arguments(
191            "`chars` requires `this` argument",
192            None,
193        ));
194    };
195
196    let Some(this) = this.as_string() else {
197        return Err(Error::invalid_arguments(
198            "`this` argument should be a String",
199            this.range(),
200        ));
201    };
202
203    let mut exprs: Vec<Expr> = Vec::new();
204
205    for char in this.chars() {
206        exprs.push(Expr::Char(char));
207    }
208
209    Ok(Expr::array(exprs))
210}
211
212pub fn string_constructor_from_chars(args: &[Expr]) -> Result<Expr, Error> {
213    let [chars] = args else {
214        return Err(Error::invalid_arguments("requires `chars` argument", None));
215    };
216
217    let Some(exprs) = chars.as_array() else {
218        return Err(Error::invalid_arguments(
219            "`chars` argument should be a (Array Char)",
220            chars.range(),
221        ));
222    };
223
224    // #todo verify Array item type!
225
226    let mut chars: Vec<char> = Vec::new();
227
228    for expr in exprs.iter() {
229        if let Some(c) = expr.as_char() {
230            chars.push(c);
231        }
232    }
233
234    let string = String::from_iter(chars);
235
236    Ok(Expr::String(string))
237}
238
239// #todo overload for string and char!
240
241pub fn char_to_upper_case(args: &[Expr]) -> Result<Expr, Error> {
242    let [this] = args else {
243        return Err(Error::invalid_arguments(
244            "`to-upper-case` requires `this` argument",
245            None,
246        ));
247    };
248
249    let Expr::Char(this) = this.unpack() else {
250        return Err(Error::invalid_arguments(
251            "`this` argument should be a Char",
252            this.range(),
253        ));
254    };
255
256    // #todo omg...
257    let uppercased = this.to_uppercase().next().unwrap();
258
259    Ok(Expr::Char(uppercased))
260}
261
262// #insight
263// Originally the Swift-like `lowercased` was considered, `to-lower-case` was
264// preferred, as it's more consistent, allows for (let lowercased (to-lower-case str)),
265// and scales to other cases, e.g. `to-kebab-case`, `to-snake-case`, etc)
266pub fn string_to_lower_case(args: &[Expr]) -> Result<Expr, Error> {
267    let [this] = args else {
268        return Err(Error::invalid_arguments("requires `this` argument", None));
269    };
270
271    // #todo consider as_stringable?
272    let Some(this) = this.as_string() else {
273        return Err(Error::invalid_arguments(
274            "`this` argument should be a String",
275            this.range(),
276        ));
277    };
278
279    let lowercased = this.to_lowercase();
280
281    Ok(Expr::String(lowercased))
282}
283
284// #todo make this a String constructor?
285// #todo 'join' and 'format' versions?
286
287// #todo use (to-string ..) instead of format-value
288// #todo find another name, this is too common: `fmt`? `stringf`?
289// (format-string "hello {} {:.5}" name price)
290pub fn string_format(args: &[Expr]) -> Result<Expr, Error> {
291    let output = args.iter().fold(String::new(), |mut str, x| {
292        str.push_str(&format_value(x));
293        str
294    });
295
296    Ok(Expr::String(output))
297}
298
299// name: split
300// type: (Func (String String) String)
301// macro annotation: (this: String, separator: String) -> String
302// (Func (this separator) ..)
303// (Func (#String this #String separator) String)
304pub fn string_split(args: &[Expr]) -> Result<Expr, Error> {
305    // #todo Consider different order of arguments.
306    // #todo Don't use `this` name.
307    let string = unpack_stringable_arg(args, 0, "this")?;
308    let separator = unpack_stringable_arg(args, 1, "separator")?;
309
310    // #todo should return iterator
311
312    let parts: Vec<Expr> = string.split(separator).map(Expr::string).collect();
313
314    Ok(Expr::array(parts))
315}
316
317// #todo string_is_matching
318
319pub fn string_contains(args: &[Expr]) -> Result<Expr, Error> {
320    let [this, string] = args else {
321        return Err(Error::invalid_arguments(
322            "`contains` requires `this` and `string` arguments",
323            None,
324        ));
325    };
326
327    let Some(this) = this.as_string() else {
328        return Err(Error::invalid_arguments(
329            "`this` argument should be a String",
330            this.range(),
331        ));
332    };
333
334    let Some(string) = string.as_string() else {
335        return Err(Error::invalid_arguments(
336            "`string` argument should be a String",
337            string.range(),
338        ));
339    };
340
341    Ok(Expr::Bool(this.contains(string)))
342}
343
344pub fn string_starts_with(args: &[Expr]) -> Result<Expr, Error> {
345    let [this, prefix] = args else {
346        return Err(Error::invalid_arguments(
347            "`starts-with` requires `this` and `prefix` arguments",
348            None,
349        ));
350    };
351
352    let Some(this) = this.as_string() else {
353        return Err(Error::invalid_arguments(
354            "`this` argument should be a String",
355            this.range(),
356        ));
357    };
358
359    let Some(prefix) = prefix.as_string() else {
360        return Err(Error::invalid_arguments(
361            "`prefix` argument should be a String",
362            prefix.range(),
363        ));
364    };
365
366    Ok(Expr::Bool(this.starts_with(prefix)))
367}
368
369pub fn string_ends_with(args: &[Expr]) -> Result<Expr, Error> {
370    // #todo consider `suffix` instead of `postfix`.
371    let [this, postfix] = args else {
372        return Err(Error::invalid_arguments(
373            "`ends-with` requires `this` and `postfix` arguments",
374            None,
375        ));
376    };
377
378    let Some(this) = this.as_string() else {
379        return Err(Error::invalid_arguments(
380            "`this` argument should be a String",
381            this.range(),
382        ));
383    };
384
385    let Some(postfix) = postfix.as_string() else {
386        return Err(Error::invalid_arguments(
387            "`postfix` argument should be a String",
388            postfix.range(),
389        ));
390    };
391
392    Ok(Expr::Bool(this.ends_with(postfix)))
393}
394
395// #todo implement `replace-once`.
396
397// #todo support replace with array of rules or just use array spread.
398// #todo consider a separate function called `replace*` to support multiple arguments?
399// #todo or better consider compiler-optimization statically if there is only one replacement.
400// #todo IDE hint if a compiler-optimization is performed.
401// #todo could allow for multiple replacements (i.e. pairs of rules)
402// #todo different name? e.g. rewrite?
403pub fn string_replace(args: &[Expr]) -> Result<Expr, Error> {
404    // #insight _from, _to are only used to verify that there is at least one
405    let [this, _from, _to, ..] = args else {
406        return Err(Error::invalid_arguments(
407            "`replace` requires `this`, `from`, and `to` arguments",
408            None,
409        ));
410    };
411
412    let Some(this) = this.as_string() else {
413        return Err(Error::invalid_arguments(
414            "`this` argument should be a String",
415            this.range(),
416        ));
417    };
418
419    let mut output: String = this.to_string();
420
421    let mut i = 1;
422    while i < args.len() {
423        let from = &args[i];
424        let Some(from) = from.as_string() else {
425            return Err(Error::invalid_arguments(
426                "`from` argument should be a String",
427                from.range(),
428            ));
429        };
430
431        let to = &args[i + 1];
432        let Some(to) = to.as_string() else {
433            return Err(Error::invalid_arguments(
434                "`to` argument should be a String",
435                to.range(),
436            ));
437        };
438
439        output = output.replace(from, to);
440
441        i += 2;
442    }
443
444    Ok(Expr::String(output))
445
446    // let Some(from) = from.as_string() else {
447    //     return Err(Error::invalid_arguments(
448    //         "`from` argument should be a String",
449    //         from.range(),
450    //     ));
451    // };
452
453    // let Some(to) = to.as_string() else {
454    //     return Err(Error::invalid_arguments(
455    //         "`to` argument should be a String",
456    //         to.range(),
457    //     ));
458    // };
459
460    // Ok(Expr::String(this.replace(from, to)))
461}
462
463// #todo move to cmp.rs?
464// #todo should this get renamed to `stringable_compare`?
465// #todo should be associated with `Ordering` and `Comparable`.
466pub fn string_compare(args: &[Expr]) -> Result<Expr, Error> {
467    // #todo support multiple arguments.
468    let [a, b] = args else {
469        return Err(Error::invalid_arguments(
470            "requires at least two arguments",
471            None,
472        ));
473    };
474
475    // #todo is this check required if we perform type inference before calling
476    // this function?
477
478    let Some(a) = a.as_stringable() else {
479        return Err(Error::invalid_arguments(
480            &format!("{a} is not a String"),
481            a.range(),
482        ));
483    };
484
485    let Some(b) = b.as_stringable() else {
486        return Err(Error::invalid_arguments(
487            &format!("{b} is not a String"),
488            b.range(),
489        ));
490    };
491
492    // #todo temp hack until Tan has enums?
493    let ordering = match a.cmp(b) {
494        std::cmp::Ordering::Less => -1,
495        std::cmp::Ordering::Equal => 0,
496        std::cmp::Ordering::Greater => 1,
497    };
498
499    Ok(Expr::Int(ordering))
500}
501
502pub fn setup_lib_string(context: &mut Context) {
503    let module = require_module("prelude", context);
504
505    module.insert_invocable("String", Expr::foreign_func(&string_new));
506
507    // #insight it's OK if it's not short, it's not often used.
508    // #todo consider a shorter name, e.g. `Str/from`, `Str/from-chars`
509    // #todo implement as String constructor method/override?, e.g. `(Str [(Char "h")(Char "i")])`
510    module.insert_invocable(
511        "String/from-chars",
512        Expr::foreign_func(&string_constructor_from_chars),
513    );
514    // env.insert("String$$Array", Expr::foreign_func(&string_constructor_from_chars)));
515
516    module.insert_invocable("chars", Expr::foreign_func(&string_chars));
517    module.insert_invocable("chars$$String", Expr::foreign_func(&string_chars));
518
519    module.insert_invocable("is-empty?", Expr::foreign_func(&string_is_empty));
520    module.insert_invocable("is-empty?$$String", Expr::foreign_func(&string_is_empty));
521
522    // #todo rename to `to-uppercase`, more consistent?
523    module.insert_invocable("to-upper-case", Expr::foreign_func(&char_to_upper_case));
524    module.insert_invocable(
525        "to-upper-case$$Char",
526        Expr::foreign_func(&char_to_upper_case),
527    );
528
529    module.insert_invocable("to-lower-case", Expr::foreign_func(&string_to_lower_case));
530    module.insert_invocable(
531        "to-lower-case$$String",
532        Expr::foreign_func(&string_to_lower_case),
533    );
534
535    module.insert_invocable("format-string", Expr::foreign_func(&string_format));
536
537    module.insert_invocable("split", Expr::foreign_func(&string_split));
538
539    module.insert_invocable("replace", Expr::foreign_func(&string_replace));
540
541    // #todo slice is to general works both as noun and verb, try to find an explicit verb? e.g. `cut` or `carve`
542    // #todo alternatively use something like `get-slice` or `cut-slice` or `carve-slice`.
543    module.insert_invocable("slice", Expr::foreign_func(&string_slice));
544    module.insert_invocable("slice$$String$$Int$$Int", Expr::foreign_func(&string_slice));
545    module.insert_invocable(
546        "slice$$String$$(Range Int)",
547        Expr::foreign_func(&string_slice_range),
548    );
549
550    // #todo find a better name, `size`?
551    // #insight `count` is _not_ a good name, reserve it for verb/action.
552    // #todo What about count-of?
553    module.insert_invocable("get-length", Expr::foreign_func(&string_get_length));
554    module.insert_invocable("get-length$$String", Expr::foreign_func(&string_get_length));
555
556    // #todo write tan unit test
557    module.insert_invocable("trim", Expr::foreign_func(&string_trim));
558
559    // module.insert_invocable("contains?", Expr::foreign_func(&string_contains)));
560    module.insert_invocable(
561        "contains?$$String$$String",
562        Expr::foreign_func(&string_contains),
563    );
564
565    module.insert_invocable("starts-with?", Expr::foreign_func(&string_starts_with));
566
567    /*
568    (if (ends-with filename ".png")
569    (if (ends-with? filename ".png")
570        (handle-image filename)
571        (handle filename)
572    )
573     */
574    // #todo: consider 'ends-with' without '?'.
575    module.insert_invocable("ends-with?", Expr::foreign_func(&string_ends_with));
576}