1pub 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}