// UTF-8-sichere String-Truncation // // `&s[..n]` panickt wenn n mitten in einem Multi-Byte-Zeichen liegt. // Konkret: ein '✅' belegt 3 Bytes; bei `&s[..240]` wenn ✅ auf // Byte-Position 239..241 sitzt → Panic "is not a char boundary". // // Diese Funktion findet die naechste Char-Boundary <= max_bytes // und schneidet dort sauber ab. /// Schneidet `s` an einer Char-Boundary <= `max_bytes` ab. /// Wenn `s` schon kuerzer ist, wird `s` unveraendert zurueckgegeben. pub fn safe_truncate(s: &str, max_bytes: usize) -> &str { if s.len() <= max_bytes { return s; } // floor_char_boundary ist nightly; eigene Implementierung: let mut end = max_bytes; while end > 0 && !s.is_char_boundary(end) { end -= 1; } &s[..end] } /// Wie `safe_truncate`, haengt aber `…` an wenn gekuerzt wurde. pub fn safe_truncate_ellipsis(s: &str, max_bytes: usize) -> String { if s.len() <= max_bytes { s.to_string() } else { format!("{}…", safe_truncate(s, max_bytes)) } } #[cfg(test)] mod tests { use super::*; #[test] fn ascii_works() { assert_eq!(safe_truncate("hello world", 5), "hello"); } #[test] fn does_not_panic_on_emoji_boundary() { // ✅ ist 3 Bytes (E2 9C 85). Wenn wir bei Byte 1 abschneiden // wuerde &s[..1] panicken. let s = "abc✅def"; // ✅ liegt auf Byte 3..6, abschneiden bei 4 → muss auf 3 zurueck let out = safe_truncate(s, 4); assert_eq!(out, "abc"); } #[test] fn ellipsis_appends() { let s = "abcdefghij"; assert_eq!(safe_truncate_ellipsis(s, 5), "abcde…"); } #[test] fn ellipsis_no_truncate_when_short() { assert_eq!(safe_truncate_ellipsis("hi", 10), "hi"); } #[test] fn handles_german_umlaut() { // ä ist 2 Bytes. "abäc" → ab=2, ä=2 (4), c=1 (5) let s = "abäc"; // Schnitt bei 3 → ä halbiert → muss auf 2 zurueck assert_eq!(safe_truncate(s, 3), "ab"); assert_eq!(safe_truncate(s, 4), "abä"); } }