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/// Slice a list by indices (bounds-safe).
61/// Indices are inclusive start, exclusive end.
62#[must_use]
63pub fn list_slice<T: Clone>(list: Vec<T>, start: i64, end: i64) -> Vec<T> {
64    let len = list.len();
65    let start = start.max(0) as usize;
66    let end = end.max(0) as usize;
67    let start = start.min(len);
68    let end = end.min(len);
69    if start >= end {
70        return Vec::new();
71    }
72    list[start..end].to_vec()
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_list_slice() {
81        assert_eq!(list_slice(vec![1, 2, 3, 4, 5], 1, 4), vec![2, 3, 4]);
82        assert_eq!(list_slice(vec![1, 2, 3], 0, 10), vec![1, 2, 3]);
83        assert_eq!(list_slice(vec![1, 2, 3], -5, 2), vec![1, 2]);
84        assert_eq!(list_slice(vec![1, 2, 3], 5, 10), Vec::<i64>::new());
85    }
86
87    #[test]
88    fn test_str_index_of() {
89        assert_eq!(str_index_of("hello world", "world"), Some(6));
90        assert_eq!(str_index_of("hello world", "foo"), None);
91        assert_eq!(str_index_of("hello", ""), Some(0));
92        // Unicode test
93        assert_eq!(str_index_of("héllo wörld", "wörld"), Some(6));
94    }
95
96    #[test]
97    fn test_str_slice() {
98        assert_eq!(str_slice("hello", 1, 4), "ell");
99        assert_eq!(str_slice("hello", 0, 5), "hello");
100        assert_eq!(str_slice("hello", 3, 100), "lo");
101        assert_eq!(str_slice("hello", -5, 3), "hel");
102        // Unicode test
103        assert_eq!(str_slice("héllo", 0, 3), "hél");
104    }
105
106    #[test]
107    fn test_str_pad_start() {
108        assert_eq!(str_pad_start("5", 3, "0"), "005");
109        assert_eq!(str_pad_start("hello", 3, "x"), "hello");
110        assert_eq!(str_pad_start("a", 5, "xy"), "xyxya");
111    }
112
113    #[test]
114    fn test_str_pad_end() {
115        assert_eq!(str_pad_end("5", 3, "0"), "500");
116        assert_eq!(str_pad_end("hello", 3, "x"), "hello");
117        assert_eq!(str_pad_end("a", 5, "xy"), "axyxy");
118    }
119}