Skip to main content

aver/types/
string.rs

1/// String namespace — text manipulation helpers.
2///
3/// Methods:
4///   String.len(s)               → Int            — char count (code points)
5///   String.byteLength(s)        → Int            — byte count (UTF-8)
6///   String.startsWith(s, pre)   → Bool
7///   String.endsWith(s, suf)     → Bool
8///   String.contains(s, sub)     → Bool
9///   String.slice(s, from, to)   → String         — code-point based substring
10///   String.trim(s)              → String
11///   String.split(s, delim)      → List<String>
12///   String.replace(s, old, new) → String
13///   String.join(list, sep)      → String
14///   String.charAt(s, index)     → Option<String>  — O(1)-ish char at code-point index
15///   String.chars(s)             → List<String>   — each char as 1-char string
16///   String.fromInt(n)           → String
17///   String.fromFloat(f)         → String
18///   String.fromBool(b)          → String
19///   String.toLower(s)           → String         — lowercase (Unicode-aware)
20///   String.toUpper(s)           → String         — uppercase (Unicode-aware)
21///
22/// No effects required.
23use std::collections::HashMap;
24
25use crate::value::{RuntimeError, Value, list_from_vec, list_slice};
26
27pub fn register(global: &mut HashMap<String, Value>) {
28    let mut members = HashMap::new();
29    for method in &[
30        "len",
31        "byteLength",
32        "startsWith",
33        "endsWith",
34        "contains",
35        "slice",
36        "trim",
37        "split",
38        "replace",
39        "join",
40        "charAt",
41        "chars",
42        "fromInt",
43        "fromFloat",
44        "fromBool",
45        "toLower",
46        "toUpper",
47    ] {
48        members.insert(
49            method.to_string(),
50            Value::Builtin(format!("String.{}", method)),
51        );
52    }
53    global.insert(
54        "String".to_string(),
55        Value::Namespace {
56            name: "String".to_string(),
57            members,
58        },
59    );
60}
61
62pub fn effects(_name: &str) -> &'static [&'static str] {
63    &[]
64}
65
66/// Returns `Some(result)` when `name` is owned by this namespace, `None` otherwise.
67pub fn call(name: &str, args: &[Value]) -> Option<Result<Value, RuntimeError>> {
68    match name {
69        "String.len" => Some(length(&args)),
70        "String.byteLength" => Some(byte_length(&args)),
71        "String.startsWith" => Some(starts_with(&args)),
72        "String.endsWith" => Some(ends_with(&args)),
73        "String.contains" => Some(contains(&args)),
74        "String.slice" => Some(slice(&args)),
75        "String.trim" => Some(trim(&args)),
76        "String.split" => Some(split(&args)),
77        "String.replace" => Some(replace(&args)),
78        "String.join" => Some(join(&args)),
79        "String.charAt" => Some(char_at(&args)),
80        "String.chars" => Some(chars(&args)),
81        "String.fromInt" => Some(from_int(&args)),
82        "String.fromFloat" => Some(from_float(&args)),
83        "String.fromBool" => Some(from_bool(&args)),
84        "String.toLower" => Some(to_lower(&args)),
85        "String.toUpper" => Some(to_upper(&args)),
86        _ => None,
87    }
88}
89
90// ─── Implementations ────────────────────────────────────────────────────────
91
92fn length(args: &[Value]) -> Result<Value, RuntimeError> {
93    let [val] = one_arg("String.len", args)?;
94    let Value::Str(s) = val else {
95        return Err(RuntimeError::Error(
96            "String.len: argument must be a String".to_string(),
97        ));
98    };
99    Ok(Value::Int(s.chars().count() as i64))
100}
101
102fn byte_length(args: &[Value]) -> Result<Value, RuntimeError> {
103    let [val] = one_arg("String.byteLength", args)?;
104    let Value::Str(s) = val else {
105        return Err(RuntimeError::Error(
106            "String.byteLength: argument must be a String".to_string(),
107        ));
108    };
109    Ok(Value::Int(s.len() as i64))
110}
111
112fn starts_with(args: &[Value]) -> Result<Value, RuntimeError> {
113    let [a, b] = two_args("String.startsWith", args)?;
114    let (Value::Str(s), Value::Str(prefix)) = (a, b) else {
115        return Err(RuntimeError::Error(
116            "String.startsWith: both arguments must be String".to_string(),
117        ));
118    };
119    Ok(Value::Bool(s.starts_with(prefix.as_str())))
120}
121
122fn ends_with(args: &[Value]) -> Result<Value, RuntimeError> {
123    let [a, b] = two_args("String.endsWith", args)?;
124    let (Value::Str(s), Value::Str(suffix)) = (a, b) else {
125        return Err(RuntimeError::Error(
126            "String.endsWith: both arguments must be String".to_string(),
127        ));
128    };
129    Ok(Value::Bool(s.ends_with(suffix.as_str())))
130}
131
132fn contains(args: &[Value]) -> Result<Value, RuntimeError> {
133    let [a, b] = two_args("String.contains", args)?;
134    let (Value::Str(s), Value::Str(sub)) = (a, b) else {
135        return Err(RuntimeError::Error(
136            "String.contains: both arguments must be String".to_string(),
137        ));
138    };
139    Ok(Value::Bool(s.contains(sub.as_str())))
140}
141
142fn slice(args: &[Value]) -> Result<Value, RuntimeError> {
143    if args.len() != 3 {
144        return Err(RuntimeError::Error(format!(
145            "String.slice() takes 3 arguments (s, from, to), got {}",
146            args.len()
147        )));
148    }
149    let Value::Str(s) = &args[0] else {
150        return Err(RuntimeError::Error(
151            "String.slice: first argument must be a String".to_string(),
152        ));
153    };
154    let Value::Int(from) = &args[1] else {
155        return Err(RuntimeError::Error(
156            "String.slice: second argument must be an Int".to_string(),
157        ));
158    };
159    let Value::Int(to) = &args[2] else {
160        return Err(RuntimeError::Error(
161            "String.slice: third argument must be an Int".to_string(),
162        ));
163    };
164    let from = *from as usize;
165    let to = *to as usize;
166    let result: String = s.chars().skip(from).take(to.saturating_sub(from)).collect();
167    Ok(Value::Str(result))
168}
169
170fn trim(args: &[Value]) -> Result<Value, RuntimeError> {
171    let [val] = one_arg("String.trim", args)?;
172    let Value::Str(s) = val else {
173        return Err(RuntimeError::Error(
174            "String.trim: argument must be a String".to_string(),
175        ));
176    };
177    Ok(Value::Str(s.trim().to_string()))
178}
179
180fn split(args: &[Value]) -> Result<Value, RuntimeError> {
181    let [a, b] = two_args("String.split", args)?;
182    let (Value::Str(s), Value::Str(delim)) = (a, b) else {
183        return Err(RuntimeError::Error(
184            "String.split: both arguments must be String".to_string(),
185        ));
186    };
187    let parts: Vec<Value> = s
188        .split(delim.as_str())
189        .map(|p| Value::Str(p.to_string()))
190        .collect();
191    Ok(list_from_vec(parts))
192}
193
194fn replace(args: &[Value]) -> Result<Value, RuntimeError> {
195    if args.len() != 3 {
196        return Err(RuntimeError::Error(format!(
197            "String.replace() takes 3 arguments (s, old, new), got {}",
198            args.len()
199        )));
200    }
201    let (Value::Str(s), Value::Str(old), Value::Str(new)) = (&args[0], &args[1], &args[2]) else {
202        return Err(RuntimeError::Error(
203            "String.replace: all arguments must be String".to_string(),
204        ));
205    };
206    Ok(Value::Str(s.replace(old.as_str(), new.as_str())))
207}
208
209fn join(args: &[Value]) -> Result<Value, RuntimeError> {
210    let [a, b] = two_args("String.join", args)?;
211    let items = list_slice(a).ok_or_else(|| {
212        RuntimeError::Error("String.join: first argument must be a List".to_string())
213    })?;
214    let Value::Str(sep) = b else {
215        return Err(RuntimeError::Error(
216            "String.join: second argument must be a String".to_string(),
217        ));
218    };
219    let strs: Result<Vec<String>, RuntimeError> = items
220        .iter()
221        .map(|v| match v {
222            Value::Str(s) => Ok(s.clone()),
223            _ => Err(RuntimeError::Error(
224                "String.join: list elements must be String".to_string(),
225            )),
226        })
227        .collect();
228    Ok(Value::Str(strs?.join(sep.as_str())))
229}
230
231fn char_at(args: &[Value]) -> Result<Value, RuntimeError> {
232    let [a, b] = two_args("String.charAt", args)?;
233    let Value::Str(s) = a else {
234        return Err(RuntimeError::Error(
235            "String.charAt: first argument must be a String".to_string(),
236        ));
237    };
238    let Value::Int(idx) = b else {
239        return Err(RuntimeError::Error(
240            "String.charAt: second argument must be an Int".to_string(),
241        ));
242    };
243    let idx = *idx as usize;
244    match s.chars().nth(idx) {
245        Some(c) => Ok(Value::Some(Box::new(Value::Str(c.to_string())))),
246        None => Ok(Value::None),
247    }
248}
249
250fn chars(args: &[Value]) -> Result<Value, RuntimeError> {
251    let [val] = one_arg("String.chars", args)?;
252    let Value::Str(s) = val else {
253        return Err(RuntimeError::Error(
254            "String.chars: argument must be a String".to_string(),
255        ));
256    };
257    let result: Vec<Value> = s.chars().map(|c| Value::Str(c.to_string())).collect();
258    Ok(list_from_vec(result))
259}
260
261fn from_int(args: &[Value]) -> Result<Value, RuntimeError> {
262    let [val] = one_arg("String.fromInt", args)?;
263    let Value::Int(n) = val else {
264        return Err(RuntimeError::Error(
265            "String.fromInt: argument must be an Int".to_string(),
266        ));
267    };
268    Ok(Value::Str(format!("{}", n)))
269}
270
271fn from_float(args: &[Value]) -> Result<Value, RuntimeError> {
272    let [val] = one_arg("String.fromFloat", args)?;
273    let Value::Float(f) = val else {
274        return Err(RuntimeError::Error(
275            "String.fromFloat: argument must be a Float".to_string(),
276        ));
277    };
278    Ok(Value::Str(format!("{}", f)))
279}
280
281fn from_bool(args: &[Value]) -> Result<Value, RuntimeError> {
282    let [val] = one_arg("String.fromBool", args)?;
283    let Value::Bool(b) = val else {
284        return Err(RuntimeError::Error(
285            "String.fromBool: argument must be a Bool".to_string(),
286        ));
287    };
288    Ok(Value::Str(if *b { "true" } else { "false" }.to_string()))
289}
290
291fn to_lower(args: &[Value]) -> Result<Value, RuntimeError> {
292    let [val] = one_arg("String.toLower", args)?;
293    let Value::Str(s) = val else {
294        return Err(RuntimeError::Error(
295            "String.toLower: argument must be a String".to_string(),
296        ));
297    };
298    Ok(Value::Str(s.to_lowercase()))
299}
300
301fn to_upper(args: &[Value]) -> Result<Value, RuntimeError> {
302    let [val] = one_arg("String.toUpper", args)?;
303    let Value::Str(s) = val else {
304        return Err(RuntimeError::Error(
305            "String.toUpper: argument must be a String".to_string(),
306        ));
307    };
308    Ok(Value::Str(s.to_uppercase()))
309}
310
311// ─── Helpers ────────────────────────────────────────────────────────────────
312
313fn one_arg<'a>(name: &str, args: &'a [Value]) -> Result<[&'a Value; 1], RuntimeError> {
314    if args.len() != 1 {
315        return Err(RuntimeError::Error(format!(
316            "{}() takes 1 argument, got {}",
317            name,
318            args.len()
319        )));
320    }
321    Ok([&args[0]])
322}
323
324fn two_args<'a>(name: &str, args: &'a [Value]) -> Result<[&'a Value; 2], RuntimeError> {
325    if args.len() != 2 {
326        return Err(RuntimeError::Error(format!(
327            "{}() takes 2 arguments, got {}",
328            name,
329            args.len()
330        )));
331    }
332    Ok([&args[0], &args[1]])
333}