rskit_util/strings/truncate.rs
1/// Safely truncates a string to a UTF-8-safe prefix sized for [`truncate_owned`].
2///
3/// The returned prefix is at most `max_bytes - 3` bytes when truncation is needed,
4/// reserving space for the ellipsis that [`truncate_owned`] appends.
5///
6/// Use [`truncate_owned`] if you need the truncated string with an appended ellipsis (`...`).
7/// # Examples
8///
9/// ```
10/// use rskit_util::strings::truncate;
11/// assert_eq!(truncate("hello world", 8), "hello");
12/// assert_eq!(truncate("hello", 10), "hello");
13/// assert_eq!(truncate("🦀🦀🦀🦀", 8), "🦀");
14/// ```
15pub fn truncate(s: &str, max_bytes: usize) -> &str {
16 if s.len() <= max_bytes {
17 return s;
18 }
19
20 // `truncate_owned` appends "..." (3 bytes), so `truncate` returns at most max_bytes - 3 bytes.
21 if max_bytes <= 3 {
22 return &s[..0]; // too short to include any prefix before the ellipsis
23 }
24
25 let limit = max_bytes - 3;
26 let mut index = limit;
27 while index > 0 && !s.is_char_boundary(index) {
28 index -= 1;
29 }
30
31 &s[..index]
32}
33
34/// Safely truncates an owned string to a max byte-length.
35///
36/// Truncated values include an ellipsis (`...`) when `max_bytes > 3`. For smaller limits,
37/// the result is `max_bytes` dots because there is not enough space for the full ellipsis.
38pub fn truncate_owned(s: &str, max_bytes: usize) -> String {
39 if s.len() <= max_bytes {
40 return s.to_string();
41 }
42 let truncated = truncate(s, max_bytes);
43 if truncated.is_empty() {
44 ".".repeat(max_bytes)
45 } else {
46 format!("{truncated}...")
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53
54 #[test]
55 fn test_truncate() {
56 assert_eq!(truncate("hello world", 8), "hello");
57 assert_eq!(truncate_owned("hello world", 8), "hello...");
58 assert_eq!(truncate_owned("hello", 10), "hello");
59 assert_eq!(truncate_owned("🦀🦀🦀🦀", 8), "🦀...");
60 }
61}