//! Small text helpers shared by the modules that log what the model answered and //! bound what the tools return to it. /// First `limit` characters of `text`, for logs. /// /// Cuts on character boundaries, so the excerpt stays valid UTF-8, and marks a /// truncation with an ellipsis. pub fn excerpt(text: &str, limit: usize) -> String { let mut chars = text.chars(); let excerpt = chars.by_ref().take(limit).collect::(); if chars.next().is_some() { return format!("{excerpt}…"); } excerpt } /// Truncates `text` in place to at most `limit` bytes, cutting on a character /// boundary so the result stays valid UTF-8. /// /// Returns `true` when something was dropped. pub fn truncate_bytes(text: &mut String, limit: usize) -> bool { if text.len() <= limit { return false; } let mut end = limit; while !text.is_char_boundary(end) { end -= 1; } text.truncate(end); true } #[cfg(test)] mod tests { use super::*; #[test] fn a_short_text_is_kept_as_is() { assert_eq!(excerpt("abc", 3), "abc"); } #[test] fn a_long_text_is_truncated_and_marked() { assert_eq!(excerpt("abcdef", 3), "abc…"); } #[test] fn truncation_cuts_on_character_boundaries() { assert_eq!(excerpt("ééé", 2), "éé…"); } #[test] fn a_short_text_is_not_truncated() { let mut text = String::from("abc"); assert!(!truncate_bytes(&mut text, 3)); assert_eq!(text, "abc"); } #[test] fn a_long_text_is_truncated_within_the_limit() { let mut text = String::from("abcdef"); assert!(truncate_bytes(&mut text, 4)); assert_eq!(text, "abcd"); } #[test] fn byte_truncation_never_splits_a_character() { let mut text = String::from("ééé"); // The limit falls in the middle of the second `é`: it is dropped. assert!(truncate_bytes(&mut text, 3)); assert_eq!(text, "é"); } }