Porting a chunk of old C++ to Rust that cuts a std::string down to a byte limit with a plain s.substr(0, n), and I figured it was one line. In C++ it sort of worked, even though std::string knows nothing about UTF-8 and will happily cut a letter in half, and in Cyrillic every letter is two bytes, so with an odd limit the tail came out broken and nobody noticed, because apparently nothing decoded those bytes afterwards. Then Rust hit the first Russian name, did &s[..n] and died with byte index 5 is not a char boundary, and that's where I started doubting everything.
I see a few options. Walk back from n until s.is_char_boundary(i) returns true, which is at most three steps, since a UTF-8 character is never longer than four bytes. Or go through char_indices() and take the last position where a whole character still fits. And if the limit isn't really about bytes but about what a person sees, then both are wrong, and honestly I don't know where that limit came from in the first place, a column width in the database, I think, and if it's the database then maybe the database should be the one counting.
For now it's the loop.
fn cut(s: &str, n: usize) -> &str {
if n >= s.len() {
return s;
}
let mut i = n;
while !s.is_char_boundary(i) {
i -= 1;
}
&s[..i]
}
cut("Привет", 5) gives Пр, four bytes, and a limit of zero gives an empty string instead of a panic. It's short enough that I can say what every line does, which right now matters to me more than being clever.