/// Safely truncate a string at character boundaries, not byte boundaries /// /// This function ensures that multi-byte UTF-8 characters (like Japanese, emoji, etc.) /// are not split in the middle, which would cause a panic. /// /// # Arguments /// * `s` - The string to truncate /// * `max_chars` - Maximum number of characters to keep /// /// # Returns /// A truncated string with "..." appended if truncation occurred pub fn safe_truncate(s: &str, max_chars: usize) -> String { if s.chars().count() <= max_chars { s.to_string() } else { let truncated: String = s.chars().take(max_chars.saturating_sub(3)).collect(); format!("{}...", truncated) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_safe_truncate_ascii() { assert_eq!(safe_truncate("hello world", 20), "hello world"); assert_eq!(safe_truncate("hello world", 8), "hello..."); assert_eq!(safe_truncate("hello", 5), "hello"); assert_eq!(safe_truncate("hello", 3), "..."); } #[test] fn test_safe_truncate_japanese() { // Japanese characters: "こんにちは世界" (Hello World) let japanese = "こんにちは世界"; assert_eq!(safe_truncate(japanese, 10), japanese); assert_eq!(safe_truncate(japanese, 5), "こん..."); assert_eq!(safe_truncate(japanese, 7), japanese); } #[test] fn test_safe_truncate_mixed() { // Mixed ASCII and Japanese let mixed = "Hello こんにちは"; assert_eq!(safe_truncate(mixed, 20), mixed); assert_eq!(safe_truncate(mixed, 8), "Hello..."); } }