const TRUNCATION_MARKER: &str = "... (truncated)";
pub fn truncate_str(s: &str, max_bytes: usize) -> String {
if s.len() <= max_bytes {
return s.to_string();
}
let cut = s
.char_indices()
.map(|(i, _)| i)
.take_while(|&i| i <= max_bytes)
.last()
.unwrap_or(0);
format!("{}{TRUNCATION_MARKER}", &s[..cut])
}
pub fn truncate_string(mut s: String, max_bytes: usize) -> String {
if s.len() <= max_bytes {
return s;
}
let cut = s
.char_indices()
.map(|(i, _)| i)
.take_while(|&i| i <= max_bytes)
.last()
.unwrap_or(0);
s.truncate(cut);
s.push_str(TRUNCATION_MARKER);
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_truncation_at_or_below_limit() {
let exact = "a".repeat(10);
assert_eq!(truncate_str(&exact, 10), exact);
assert_eq!(truncate_str("", 10), "");
}
#[test]
fn truncates_just_above_limit() {
let message = "a".repeat(11);
assert_eq!(
truncate_str(&message, 10),
format!("{}... (truncated)", "a".repeat(10))
);
}
#[test]
fn handles_multibyte_char_boundary() {
let message = "é".repeat(10);
let truncated = truncate_str(&message, 5);
assert!(truncated.starts_with(&"é".repeat(2)));
assert!(truncated.ends_with("... (truncated)"));
}
#[test]
fn truncate_string_no_truncation_at_or_below_limit() {
let exact = "a".repeat(10);
assert_eq!(truncate_string(exact.clone(), 10), exact);
assert_eq!(truncate_string(String::new(), 10), "");
}
#[test]
fn truncate_string_truncates_just_above_limit() {
let message = "a".repeat(11);
assert_eq!(
truncate_string(message, 10),
format!("{}... (truncated)", "a".repeat(10))
);
}
#[test]
fn truncate_string_handles_multibyte_char_boundary() {
let message = "é".repeat(10);
let truncated = truncate_string(message, 5);
assert!(truncated.starts_with(&"é".repeat(2)));
assert!(truncated.ends_with("... (truncated)"));
}
#[test]
fn truncate_str_and_truncate_string_agree() {
let message = "x".repeat(500);
assert_eq!(truncate_str(&message, 100), truncate_string(message, 100));
}
}