#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UtfIndexClamp {
Down,
Up,
}
pub fn utf16_offset_to_utf8_byte_offset(
text: &str,
utf16_offset: usize,
clamp: UtfIndexClamp,
) -> usize {
let target = utf16_offset;
let mut utf16_units = 0usize;
let mut last_byte = 0usize;
for (byte, ch) in text.char_indices() {
if utf16_units == target {
return byte;
}
if utf16_units > target {
return match clamp {
UtfIndexClamp::Down => last_byte,
UtfIndexClamp::Up => byte,
};
}
last_byte = byte;
utf16_units = utf16_units.saturating_add(ch.len_utf16());
if utf16_units == target {
return byte + ch.len_utf8();
}
if utf16_units > target {
return match clamp {
UtfIndexClamp::Down => byte,
UtfIndexClamp::Up => byte + ch.len_utf8(),
};
}
}
if utf16_units <= target {
text.len()
} else {
match clamp {
UtfIndexClamp::Down => text.len(),
UtfIndexClamp::Up => text.len(),
}
}
}
pub fn utf8_byte_offset_to_utf16_offset(
text: &str,
utf8_offset: usize,
clamp: UtfIndexClamp,
) -> usize {
let target = utf8_offset.min(text.len());
if target == 0 {
return 0;
}
let mut utf16_units = 0usize;
for (byte_start, ch) in text.char_indices() {
let byte_end = byte_start + ch.len_utf8();
let utf16_start = utf16_units;
let utf16_end = utf16_start + ch.len_utf16();
if target == byte_start {
return utf16_start;
}
if target > byte_start && target < byte_end {
return match clamp {
UtfIndexClamp::Down => utf16_start,
UtfIndexClamp::Up => utf16_end,
};
}
utf16_units = utf16_end;
}
utf16_units
}
pub fn utf16_range_to_utf8_byte_range(
text: &str,
start_utf16: usize,
end_utf16: usize,
) -> (usize, usize) {
let start = utf16_offset_to_utf8_byte_offset(text, start_utf16, UtfIndexClamp::Down);
let end = utf16_offset_to_utf8_byte_offset(text, end_utf16, UtfIndexClamp::Up);
(start.min(end), end.max(start))
}
pub fn utf8_byte_range_to_utf16_range(
text: &str,
start_utf8: usize,
end_utf8: usize,
) -> (usize, usize) {
let start = utf8_byte_offset_to_utf16_offset(text, start_utf8, UtfIndexClamp::Down);
let end = utf8_byte_offset_to_utf16_offset(text, end_utf8, UtfIndexClamp::Up);
(start.min(end), end.max(start))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn utf16_to_utf8_ascii_roundtrips() {
let s = "hello";
for i in 0..=5 {
let b = utf16_offset_to_utf8_byte_offset(s, i, UtfIndexClamp::Down);
assert_eq!(b, i);
let u16 = utf8_byte_offset_to_utf16_offset(s, b, UtfIndexClamp::Down);
assert_eq!(u16, i);
}
}
#[test]
fn utf16_to_utf8_surrogate_pair_clamps() {
let s = "a😀b";
assert_eq!(
utf16_offset_to_utf8_byte_offset(s, 0, UtfIndexClamp::Down),
0
);
assert_eq!(
utf16_offset_to_utf8_byte_offset(s, 1, UtfIndexClamp::Down),
1
);
assert_eq!(
utf16_offset_to_utf8_byte_offset(s, 2, UtfIndexClamp::Down),
1
);
assert_eq!(
utf16_offset_to_utf8_byte_offset(s, 2, UtfIndexClamp::Up),
1 + "😀".len()
);
assert_eq!(
utf16_offset_to_utf8_byte_offset(s, 3, UtfIndexClamp::Down),
1 + "😀".len()
);
assert_eq!(
utf16_offset_to_utf8_byte_offset(s, 4, UtfIndexClamp::Down),
s.len()
);
}
#[test]
fn utf16_range_converts_to_valid_utf8_range() {
let s = "a😀b";
let (bs, be) = utf16_range_to_utf8_byte_range(s, 1, 3);
assert_eq!(&s[bs..be], "😀");
let (bs, be) = utf16_range_to_utf8_byte_range(s, 2, 2);
assert!(bs <= be);
assert!(s.is_char_boundary(bs));
assert!(s.is_char_boundary(be));
}
#[test]
fn utf8_to_utf16_clamps_inside_codepoint() {
let s = "a😀b";
let inside = 2;
assert_eq!(
utf8_byte_offset_to_utf16_offset(s, inside, UtfIndexClamp::Down),
1
);
assert_eq!(
utf8_byte_offset_to_utf16_offset(s, inside, UtfIndexClamp::Up),
3
);
}
}