Skip to main content

ytil_ext/
string_ext.rs

1//! Extensions for [`String`] and [`str`]
2
3pub trait StringExt {
4    fn trim_end_at_with(&self, at: usize, with: Option<&str>) -> String;
5}
6
7impl<T: AsRef<str>> StringExt for T {
8    fn trim_end_at_with(&self, at: usize, with: Option<&str>) -> String {
9        let normalized = self.as_ref().split_whitespace().collect::<Vec<_>>().join(" ");
10        let chars: Vec<char> = normalized.chars().collect();
11
12        if chars.len() <= at {
13            return normalized;
14        }
15
16        if at == 0 {
17            return String::new();
18        }
19
20        if at == 1 {
21            return "…".to_owned();
22        }
23
24        let mut trimmed: String = chars.into_iter().take(at.saturating_sub(1)).collect();
25        trimmed.push_str(with.unwrap_or("…"));
26        trimmed
27    }
28}
29
30#[cfg(test)]
31mod tests {
32    use test_that::prelude::*;
33
34    use super::*;
35
36    #[rstest::rstest]
37    #[case("hello world", 20, None, "hello world")]
38    #[case("abcdefghijklmnopqrstuvwxyz", 5, None, "abcd…")]
39    #[case("abc", 1, None, "…")]
40    #[case("abc", 0, None, "")]
41    #[case("abcdefghijklmnopqrstuvwxyz", 5, Some("!"), "abcd!")]
42    fn test_trim_end_at_with_trims_as_expected(
43        #[case] value: &str,
44        #[case] max_chars: usize,
45        #[case] with: Option<&str>,
46        #[case] expected: &str,
47    ) {
48        assert_that!(value.trim_end_at_with(max_chars, with), eq(expected));
49    }
50}