Skip to main content

zsh/extensions/
stringsort.rs

1//! String manipulation and sorting for zshrs
2
3use std::cmp::Ordering;
4
5/// Duplicate a string (equivalent to dupstring/ztrdup in C)
6#[inline]
7pub fn dupstring(s: &str) -> String {
8    s.to_string()
9}
10
11/// Duplicate a string with a specified length
12pub fn dupstring_wlen(s: &str, len: usize) -> String {
13    if len >= s.len() {
14        s.to_string()
15    } else {
16        s[..len].to_string()
17    }
18}
19
20/// Concatenate three strings
21pub fn tricat(s1: &str, s2: &str, s3: &str) -> String {
22    let mut result = String::with_capacity(s1.len() + s2.len() + s3.len());
23    result.push_str(s1);
24    result.push_str(s2);
25    result.push_str(s3);
26    result
27}
28
29/// Concatenate two strings
30pub fn bicat(s1: &str, s2: &str) -> String {
31    let mut result = String::with_capacity(s1.len() + s2.len());
32    result.push_str(s1);
33    result.push_str(s2);
34    result
35}
36
37/// Duplicate a prefix of a string
38pub fn dupstrpfx(s: &str, len: usize) -> String {
39    dupstring_wlen(s, len)
40}
41
42/// Append a string to another, returning the result
43pub fn appstr(base: &str, append: &str) -> String {
44    bicat(base, append)
45}
46
47/// Get pointer to the last character of a string
48pub fn strend(s: &str) -> Option<char> {
49    s.chars().last()
50}
51
52/// The canonical `SORTIT_*` bits live in `zsh.h` (ported at
53/// `crate::ported::zsh_h`). This module used to declare its own set with
54/// DIFFERENT values — `SORTIT_BACKWARDS = 1` / `SORTIT_IGNORING_CASE = 8`,
55/// i.e. those two swapped relative to c:Src/zsh.h:2360-2366 — so any flag
56/// word crossing between the two vocabularies silently changed meaning.
57/// Re-export the real ones instead of redeclaring them.
58pub mod sort_flags {
59    use crate::ported::zsh_h;
60
61    /// c:Src/zsh.h — `SORTIT_BACKWARDS`.
62    pub const SORTIT_BACKWARDS: u32 = zsh_h::SORTIT_BACKWARDS as u32;
63    /// c:Src/zsh.h — `SORTIT_NUMERICALLY`.
64    pub const SORTIT_NUMERICALLY: u32 = zsh_h::SORTIT_NUMERICALLY as u32;
65    /// c:Src/zsh.h — `SORTIT_NUMERICALLY_SIGNED`.
66    pub const SORTIT_NUMERICALLY_SIGNED: u32 = zsh_h::SORTIT_NUMERICALLY_SIGNED as u32;
67    /// c:Src/zsh.h — `SORTIT_IGNORING_CASE`.
68    pub const SORTIT_IGNORING_CASE: u32 = zsh_h::SORTIT_IGNORING_CASE as u32;
69    /// c:Src/zsh.h — `SORTIT_IGNORING_BACKSLASHES`.
70    pub const SORTIT_IGNORING_BACKSLASHES: u32 = zsh_h::SORTIT_IGNORING_BACKSLASHES as u32;
71}
72
73/// Compare two strings under the `SORTIT_*` bits, delegating to the
74/// canonical port of `zstrcmp` (`Src/sort.c:191`) at
75/// `crate::ported::sort::zstrcmp`.
76///
77/// This function previously carried a SECOND, from-scratch comparator with
78/// its own `numeric_compare` / `extract_number` helpers. That copy had the
79/// same defect the ported one was fixed for in `Src/sort.c:155` — it entered
80/// the numeric branch on `a_is_digit || b_is_digit` where C requires
81/// `idigit(*as) && idigit(*bs)`, so a digit facing a non-digit compared as a
82/// number against 0 instead of falling through to collation. It had no
83/// callers, which is exactly why the defect survived: nothing exercised it.
84/// Route to the one comparator rather than keep a divergent twin alive.
85pub fn zstrcmp(a: &str, b: &str, flags: u32) -> Ordering {
86    // c:Src/sort.c:290-295/329 — SORTIT_IGNORING_CASE is NOT handled by the
87    // comparator in C; `strmetasort` case-folds when it builds each element's
88    // `cmp` key and hands the comparator the folded text. Do the same here so
89    // the delegate stays the unmodified C comparator.
90    if flags & sort_flags::SORTIT_IGNORING_CASE != 0 {
91        return crate::ported::sort::zstrcmp(&a.to_lowercase(), &b.to_lowercase(), flags);
92    }
93    crate::ported::sort::zstrcmp(a, b, flags)
94}
95
96/// Sort an array of strings with various options
97pub fn strmetasort(array: &mut [String], flags: u32) {
98    if array.len() < 2 {
99        return;
100    }
101
102    let backwards = flags & sort_flags::SORTIT_BACKWARDS != 0;
103
104    array.sort_by(|a, b| {
105        let cmp = zstrcmp(a, b, flags);
106        if backwards {
107            cmp.reverse()
108        } else {
109            cmp
110        }
111    });
112}
113
114/// Sort string slices with various options
115pub fn sort_strings(array: &mut [&str], flags: u32) {
116    if array.len() < 2 {
117        return;
118    }
119
120    let backwards = flags & sort_flags::SORTIT_BACKWARDS != 0;
121
122    array.sort_by(|a, b| {
123        let cmp = zstrcmp(a, b, flags);
124        if backwards {
125            cmp.reverse()
126        } else {
127            cmp
128        }
129    });
130}
131
132/// Natural sort comparison (numbers sorted numerically within strings)
133pub fn natural_cmp(a: &str, b: &str) -> Ordering {
134    zstrcmp(a, b, sort_flags::SORTIT_NUMERICALLY)
135}
136
137/// Case-insensitive comparison
138pub fn strcasecmp(a: &str, b: &str) -> Ordering {
139    a.to_lowercase().cmp(&b.to_lowercase())
140}
141
142/// Find first occurrence of substring
143pub fn strstr(haystack: &str, needle: &str) -> Option<usize> {
144    haystack.find(needle)
145}
146
147/// Check if string starts with prefix
148pub fn strprefix(s: &str, prefix: &str) -> bool {
149    s.starts_with(prefix)
150}
151
152/// Check if string ends with suffix
153pub fn strsuffix(s: &str, suffix: &str) -> bool {
154    s.ends_with(suffix)
155}
156
157/// Join strings with a separator
158pub fn strjoin<I, S>(iter: I, sep: &str) -> String
159where
160    I: IntoIterator<Item = S>,
161    S: AsRef<str>,
162{
163    iter.into_iter()
164        .map(|s| s.as_ref().to_string())
165        .collect::<Vec<_>>()
166        .join(sep)
167}
168
169/// Split string by separator
170pub fn strsplit(s: &str, sep: char) -> Vec<&str> {
171    s.split(sep).collect()
172}
173
174/// Trim whitespace from both ends
175pub fn strtrim(s: &str) -> &str {
176    s.trim()
177}
178
179/// Convert string to lowercase
180pub fn strlower(s: &str) -> String {
181    s.to_lowercase()
182}
183
184/// Convert string to uppercase
185pub fn strupper(s: &str) -> String {
186    s.to_uppercase()
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_dupstring() {
195        let _g = crate::test_util::global_state_lock();
196        assert_eq!(dupstring("hello"), "hello");
197        assert_eq!(dupstring(""), "");
198    }
199
200    #[test]
201    fn test_dupstring_wlen() {
202        let _g = crate::test_util::global_state_lock();
203        assert_eq!(dupstring_wlen("hello", 3), "hel");
204        assert_eq!(dupstring_wlen("hi", 10), "hi");
205    }
206
207    #[test]
208    fn test_tricat() {
209        let _g = crate::test_util::global_state_lock();
210        assert_eq!(tricat("a", "b", "c"), "abc");
211        assert_eq!(tricat("hello", " ", "world"), "hello world");
212    }
213
214    #[test]
215    fn test_bicat() {
216        let _g = crate::test_util::global_state_lock();
217        assert_eq!(bicat("hello", "world"), "helloworld");
218    }
219
220    #[test]
221    fn test_strend() {
222        let _g = crate::test_util::global_state_lock();
223        assert_eq!(strend("hello"), Some('o'));
224        assert_eq!(strend(""), None);
225    }
226
227    #[test]
228    fn test_zstrcmp_basic() {
229        let _g = crate::test_util::global_state_lock();
230        assert_eq!(zstrcmp("abc", "abc", 0), Ordering::Equal);
231        assert_eq!(zstrcmp("abc", "abd", 0), Ordering::Less);
232        assert_eq!(zstrcmp("abd", "abc", 0), Ordering::Greater);
233    }
234
235    #[test]
236    fn test_zstrcmp_case_insensitive() {
237        let _g = crate::test_util::global_state_lock();
238        let flags = sort_flags::SORTIT_IGNORING_CASE;
239        assert_eq!(zstrcmp("ABC", "abc", flags), Ordering::Equal);
240        assert_eq!(zstrcmp("ABC", "ABD", flags), Ordering::Less);
241    }
242
243    #[test]
244    fn test_zstrcmp_ignore_backslash() {
245        let _g = crate::test_util::global_state_lock();
246        let flags = sort_flags::SORTIT_IGNORING_BACKSLASHES;
247        assert_eq!(zstrcmp("a\\bc", "abc", flags), Ordering::Equal);
248    }
249
250    #[test]
251    fn test_zstrcmp_numeric() {
252        let _g = crate::test_util::global_state_lock();
253        let flags = sort_flags::SORTIT_NUMERICALLY;
254        assert_eq!(zstrcmp("file2", "file10", flags), Ordering::Less);
255        assert_eq!(zstrcmp("file10", "file2", flags), Ordering::Greater);
256        assert_eq!(zstrcmp("file10", "file10", flags), Ordering::Equal);
257    }
258
259    #[test]
260    fn test_zstrcmp_numeric_signed() {
261        let _g = crate::test_util::global_state_lock();
262        let flags = sort_flags::SORTIT_NUMERICALLY_SIGNED;
263        assert_eq!(zstrcmp("-5", "3", flags), Ordering::Less);
264        assert_eq!(zstrcmp("-10", "-2", flags), Ordering::Less);
265    }
266
267    #[test]
268    fn test_strmetasort() {
269        let _g = crate::test_util::global_state_lock();
270        let mut arr = vec![
271            "file10".to_string(),
272            "file2".to_string(),
273            "file1".to_string(),
274        ];
275        strmetasort(&mut arr, sort_flags::SORTIT_NUMERICALLY);
276        assert_eq!(arr, vec!["file1", "file2", "file10"]);
277    }
278
279    #[test]
280    fn test_strmetasort_backwards() {
281        let _g = crate::test_util::global_state_lock();
282        let mut arr = vec!["a".to_string(), "c".to_string(), "b".to_string()];
283        strmetasort(&mut arr, sort_flags::SORTIT_BACKWARDS);
284        assert_eq!(arr, vec!["c", "b", "a"]);
285    }
286
287    #[test]
288    fn test_natural_cmp() {
289        let _g = crate::test_util::global_state_lock();
290        assert_eq!(natural_cmp("item2", "item10"), Ordering::Less);
291    }
292
293    #[test]
294    fn test_strcasecmp() {
295        let _g = crate::test_util::global_state_lock();
296        assert_eq!(strcasecmp("Hello", "HELLO"), Ordering::Equal);
297        assert_eq!(strcasecmp("abc", "ABD"), Ordering::Less);
298    }
299
300    #[test]
301    fn test_strstr() {
302        let _g = crate::test_util::global_state_lock();
303        assert_eq!(strstr("hello world", "world"), Some(6));
304        assert_eq!(strstr("hello", "xyz"), None);
305    }
306
307    #[test]
308    fn test_strprefix_suffix() {
309        let _g = crate::test_util::global_state_lock();
310        assert!(strprefix("hello", "hel"));
311        assert!(!strprefix("hello", "ell"));
312        assert!(strsuffix("hello", "llo"));
313        assert!(!strsuffix("hello", "ell"));
314    }
315
316    #[test]
317    fn test_strjoin() {
318        let _g = crate::test_util::global_state_lock();
319        assert_eq!(strjoin(["a", "b", "c"], ","), "a,b,c");
320        assert_eq!(strjoin(Vec::<&str>::new(), ","), "");
321    }
322
323    #[test]
324    fn test_strsplit() {
325        let _g = crate::test_util::global_state_lock();
326        assert_eq!(strsplit("a,b,c", ','), vec!["a", "b", "c"]);
327    }
328
329    #[test]
330    fn test_strtrim() {
331        let _g = crate::test_util::global_state_lock();
332        assert_eq!(strtrim("  hello  "), "hello");
333    }
334
335    #[test]
336    fn test_case_conversion() {
337        let _g = crate::test_util::global_state_lock();
338        assert_eq!(strlower("HeLLo"), "hello");
339        assert_eq!(strupper("HeLLo"), "HELLO");
340    }
341}