pub fn truncate_at_char_boundary(s: &str, max_bytes: usize) -> &str {
if s.len() <= max_bytes {
return s;
}
let mut end = max_bytes;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
}
pub fn truncate_with_ellipsis(s: &str, max_bytes: usize) -> String {
if s.len() <= max_bytes {
return s.to_string();
}
format!("{}...", truncate_at_char_boundary(s, max_bytes))
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn ascii_under_limit_unchanged() {
assert_eq!(truncate_at_char_boundary("hello", 10), "hello");
}
#[test]
fn ascii_over_limit_truncated() {
assert_eq!(truncate_at_char_boundary("hello world", 5), "hello");
}
#[test]
fn em_dash_does_not_panic() {
let s = "refactor: split foo — bar";
let t = truncate_at_char_boundary(s, 20);
assert!(t.len() <= 20);
assert!(!t.ends_with(char::REPLACEMENT_CHARACTER));
}
#[test]
fn truncation_at_char_boundary() {
let s = "a——b"; assert_eq!(truncate_at_char_boundary(s, 2), "a");
assert_eq!(truncate_at_char_boundary(s, 4), "a—");
assert_eq!(truncate_at_char_boundary(s, 7), "a——");
assert_eq!(truncate_at_char_boundary(s, 8), "a——b");
}
#[test]
fn ellipsis_under_limit_no_change() {
assert_eq!(truncate_with_ellipsis("short", 100), "short");
}
#[test]
fn ellipsis_over_limit_appended() {
assert_eq!(truncate_with_ellipsis("hello world", 5), "hello...");
}
#[test]
fn ellipsis_preserves_char_boundary() {
let s = "foo — bar baz quux";
let out = truncate_with_ellipsis(s, 6);
assert!(out.ends_with("..."));
assert!(out.is_char_boundary(out.len() - 3));
}
#[test]
fn empty_string() {
assert_eq!(truncate_at_char_boundary("", 10), "");
assert_eq!(truncate_with_ellipsis("", 10), "");
}
#[test]
fn zero_max() {
assert_eq!(truncate_at_char_boundary("hello", 0), "");
}
}