fn elision(omitted: usize) -> String {
format!("\n\n[{omitted} chars truncated]\n\n")
}
fn char_len(s: &str) -> usize {
s.chars().count()
}
fn nth_char_boundary_from_front(s: &str, n: usize) -> usize {
if n == 0 {
return 0;
}
s.char_indices().nth(n).map(|(i, _)| i).unwrap_or(s.len())
}
fn nth_char_boundary_from_back(s: &str, n: usize) -> usize {
if n == 0 {
return s.len();
}
let total = char_len(s);
if n >= total {
return 0;
}
nth_char_boundary_from_front(s, total - n)
}
pub fn truncate_head(s: &str, max_chars: usize) -> String {
let total = char_len(s);
if total <= max_chars {
return s.to_string();
}
let cut = nth_char_boundary_from_front(s, max_chars);
let omitted = total - max_chars;
format!("{}{}", &s[..cut], elision(omitted))
}
pub fn truncate_tail(s: &str, max_chars: usize) -> String {
let total = char_len(s);
if total <= max_chars {
return s.to_string();
}
let cut = nth_char_boundary_from_back(s, max_chars);
let omitted = total - max_chars;
format!("{}{}", elision(omitted), &s[cut..])
}
pub fn truncate_middle(s: &str, max_chars: usize) -> String {
let total = char_len(s);
if total <= max_chars {
return s.to_string();
}
let head = max_chars / 2;
let tail = max_chars - head;
let head_cut = nth_char_boundary_from_front(s, head);
let tail_cut = nth_char_boundary_from_back(s, tail);
let omitted = total - head - tail;
format!("{}{}{}", &s[..head_cut], elision(omitted), &s[tail_cut..])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn passthrough_when_under_cap() {
assert_eq!(truncate_head("hello", 100), "hello");
assert_eq!(truncate_tail("hello", 100), "hello");
assert_eq!(truncate_middle("hello", 100), "hello");
}
#[test]
fn passthrough_when_exactly_at_cap() {
assert_eq!(truncate_head("hello", 5), "hello");
assert_eq!(truncate_tail("hello", 5), "hello");
assert_eq!(truncate_middle("hello", 5), "hello");
}
#[test]
fn truncate_head_keeps_prefix() {
let s = "abcdefghij";
let out = truncate_head(s, 4);
assert!(out.starts_with("abcd"));
assert!(out.contains("6 chars truncated"));
}
#[test]
fn truncate_tail_keeps_suffix() {
let s = "abcdefghij";
let out = truncate_tail(s, 4);
assert!(out.ends_with("ghij"));
assert!(out.contains("6 chars truncated"));
}
#[test]
fn truncate_middle_keeps_both_ends() {
let s = "abcdefghij";
let out = truncate_middle(s, 4);
assert!(out.starts_with("ab"));
assert!(out.ends_with("ij"));
assert!(out.contains("6 chars truncated"));
}
#[test]
fn handles_multibyte_chars_safely() {
let s = "\u{1f980}\u{1f980}\u{1f980}\u{1f980}\u{1f980}\u{1f980}\u{1f980}\u{1f980}";
let out = truncate_head(s, 3);
assert!(out.starts_with("\u{1f980}\u{1f980}\u{1f980}"));
assert!(out.contains("5 chars truncated"));
assert!(out.is_char_boundary(out.len()));
}
#[test]
fn middle_with_odd_budget() {
let out = truncate_middle("0123456789", 5);
assert!(out.starts_with("01"));
assert!(out.ends_with("789"));
}
#[test]
fn empty_input_passthrough() {
assert_eq!(truncate_head("", 10), "");
assert_eq!(truncate_tail("", 10), "");
assert_eq!(truncate_middle("", 10), "");
}
#[test]
fn zero_max_chars_keeps_nothing() {
let out = truncate_head("hello world", 0);
assert!(!out.contains("hello"));
assert!(out.contains("11 chars truncated"));
}
#[test]
fn omitted_count_is_accurate() {
let s = "x".repeat(1000);
let out = truncate_head(&s, 100);
assert!(out.contains("900 chars truncated"));
}
}