pub fn utf16_to_utf8_offset(s: &str, utf16_offset: usize) -> usize {
let mut utf16_pos = 0;
for (utf8_pos, ch) in s.char_indices() {
if utf16_pos >= utf16_offset {
return utf8_pos;
}
utf16_pos += ch.len_utf16();
if utf16_pos > utf16_offset {
return utf8_pos;
}
}
s.len()
}
#[cfg(test)]
mod tests {
use super::utf16_to_utf8_offset;
fn convert(s: &str, loc: usize, len: usize) -> (usize, usize) {
(utf16_to_utf8_offset(s, loc), utf16_to_utf8_offset(s, loc + len))
}
#[test]
fn mid_surrogate_offset_snaps_down() {
assert_eq!(utf16_to_utf8_offset("\u{1F600}a", 1), 0);
assert_eq!(utf16_to_utf8_offset("\u{1F600}a", 2), 4);
}
#[test]
fn no_longer_inverted() {
assert_eq!(convert("a\u{1F600}b", 1, 1), (1, 1));
}
#[test]
fn prefix_preserved_on_mid_pair_collapse() {
assert_eq!(convert("a\u{1F600}b", 2, 0), (1, 1));
}
#[test]
fn out_of_bounds_clamps_to_len() {
assert_eq!(convert("\u{1F600}a", 99, 0), (5, 5));
}
#[test]
fn well_formed_inputs_are_identity() {
assert_eq!(convert("a\u{1F600}b", 3, 0), (5, 5));
assert_eq!(convert("a\u{1F600}b", 4, 0), (6, 6));
assert_eq!(convert("\u{3053}\u{3093}", 1, 1), (3, 6));
}
#[test]
fn monotone_non_decreasing() {
let s = "a\u{1F600}b\u{3053}\u{1F4A9}c";
let mut prev = 0;
for off in 0..=20 {
let cur = utf16_to_utf8_offset(s, off);
assert!(cur >= prev, "non-monotone at offset {off}: {cur} < {prev}");
assert!(cur <= s.len(), "offset {off} mapped past end: {cur} > {}", s.len());
prev = cur;
}
}
#[test]
fn cursor_offsets_use_utf16_units() {
for (text, units, expected) in [
("a😀b", 3, 5),
("😀😀", 2, 4),
("", 0, 0),
("", 1, 0),
("ab", 0, 0),
("ab", 1, 1),
("ab", 2, 2),
("ab", 3, 2),
("あい", 1, 3),
("あい", 2, 6),
("éあ", 1, 2),
("éあ", 2, 5),
("😀😀", 1, 0),
("😀😀", 3, 4),
("a😀b", 2, 1),
("a😀b", 4, 6),
("a😀b", 5, 6),
("😀😀", 4, 8),
("😀😀", usize::MAX, 8),
] {
let byte = utf16_to_utf8_offset(text, units);
assert_eq!(byte, expected, "{text:?}, UTF-16 offset {units}");
assert!(text.is_char_boundary(byte));
}
}
#[test]
fn cursor_offsets_match_decoded_utf16_prefixes() {
for text in ["", "ab", "あい", "éあ", "😀😀", "a😀b", "👨👩👧🇯🇵", "e\u{301}", "مرحبا"]
{
let encoded: Vec<u16> = text.encode_utf16().collect();
let mut previous = 0;
for units in 0..=encoded.len() + 3 {
let mut prefix = encoded[..units.min(encoded.len())].to_vec();
if matches!(prefix.last(), Some(0xd800..=0xdbff)) {
prefix.pop();
}
let expected = String::from_utf16(&prefix).unwrap().len();
let byte = utf16_to_utf8_offset(text, units);
assert_eq!(byte, expected, "{text:?}, UTF-16 offset {units}");
assert!(text.is_char_boundary(byte));
assert!(byte >= previous);
previous = byte;
}
}
}
}