Skip to main content

sage_runtime/stdlib/
string.rs

1//! String helper functions for the Sage standard library.
2
3/// Find the index of a substring within a string (Unicode-aware).
4/// Returns None if not found, Some(index) otherwise.
5#[must_use]
6pub fn str_index_of(haystack: &str, needle: &str) -> Option<i64> {
7    haystack.find(needle).map(|byte_pos| {
8        // Convert byte position to char position
9        haystack[..byte_pos].chars().count() as i64
10    })
11}
12
13/// Extract a substring by character indices (Unicode-aware).
14/// Indices are inclusive start, exclusive end.
15#[must_use]
16pub fn str_slice(s: &str, start: i64, end: i64) -> String {
17    let start = start.max(0) as usize;
18    let end = end.max(0) as usize;
19    s.chars()
20        .skip(start)
21        .take(end.saturating_sub(start))
22        .collect()
23}
24
25/// Pad a string at the start to reach the target length (Unicode-aware).
26#[must_use]
27pub fn str_pad_start(s: &str, target_len: i64, pad: &str) -> String {
28    let target_len = target_len.max(0) as usize;
29    let current_len = s.chars().count();
30    if current_len >= target_len || pad.is_empty() {
31        return s.to_string();
32    }
33    let needed = target_len - current_len;
34    let pad_chars: Vec<char> = pad.chars().collect();
35    let mut result = String::new();
36    for i in 0..needed {
37        result.push(pad_chars[i % pad_chars.len()]);
38    }
39    result.push_str(s);
40    result
41}
42
43/// Pad a string at the end to reach the target length (Unicode-aware).
44#[must_use]
45pub fn str_pad_end(s: &str, target_len: i64, pad: &str) -> String {
46    let target_len = target_len.max(0) as usize;
47    let current_len = s.chars().count();
48    if current_len >= target_len || pad.is_empty() {
49        return s.to_string();
50    }
51    let needed = target_len - current_len;
52    let pad_chars: Vec<char> = pad.chars().collect();
53    let mut result = s.to_string();
54    for i in 0..needed {
55        result.push(pad_chars[i % pad_chars.len()]);
56    }
57    result
58}
59
60/// Convert a Unicode code point to a single-character string.
61/// Returns the Unicode replacement character for invalid code points.
62#[must_use]
63pub fn chr(code: i64) -> String {
64    char::from_u32(code as u32)
65        .unwrap_or('\u{FFFD}')
66        .to_string()
67}
68
69/// Slice a list by indices (bounds-safe).
70/// Indices are inclusive start, exclusive end.
71#[must_use]
72pub fn list_slice<T: Clone>(list: Vec<T>, start: i64, end: i64) -> Vec<T> {
73    let len = list.len();
74    let start = start.max(0) as usize;
75    let end = end.max(0) as usize;
76    let start = start.min(len);
77    let end = end.min(len);
78    if start >= end {
79        return Vec::new();
80    }
81    list[start..end].to_vec()
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn test_list_slice() {
90        assert_eq!(list_slice(vec![1, 2, 3, 4, 5], 1, 4), vec![2, 3, 4]);
91        assert_eq!(list_slice(vec![1, 2, 3], 0, 10), vec![1, 2, 3]);
92        assert_eq!(list_slice(vec![1, 2, 3], -5, 2), vec![1, 2]);
93        assert_eq!(list_slice(vec![1, 2, 3], 5, 10), Vec::<i64>::new());
94    }
95
96    #[test]
97    fn test_str_index_of() {
98        assert_eq!(str_index_of("hello world", "world"), Some(6));
99        assert_eq!(str_index_of("hello world", "foo"), None);
100        assert_eq!(str_index_of("hello", ""), Some(0));
101        // Unicode test
102        assert_eq!(str_index_of("héllo wörld", "wörld"), Some(6));
103    }
104
105    #[test]
106    fn test_str_slice() {
107        assert_eq!(str_slice("hello", 1, 4), "ell");
108        assert_eq!(str_slice("hello", 0, 5), "hello");
109        assert_eq!(str_slice("hello", 3, 100), "lo");
110        assert_eq!(str_slice("hello", -5, 3), "hel");
111        // Unicode test
112        assert_eq!(str_slice("héllo", 0, 3), "hél");
113    }
114
115    #[test]
116    fn test_str_pad_start() {
117        assert_eq!(str_pad_start("5", 3, "0"), "005");
118        assert_eq!(str_pad_start("hello", 3, "x"), "hello");
119        assert_eq!(str_pad_start("a", 5, "xy"), "xyxya");
120    }
121
122    #[test]
123    fn test_str_pad_end() {
124        assert_eq!(str_pad_end("5", 3, "0"), "500");
125        assert_eq!(str_pad_end("hello", 3, "x"), "hello");
126        assert_eq!(str_pad_end("a", 5, "xy"), "axyxy");
127    }
128}