use crate::wtf8::Wtf8;
#[derive(Clone, Copy)]
struct Group {
base: usize,
ofs: [u8; 16],
}
pub struct Wtf8Index {
groups: Box<[Group]>,
}
impl Wtf8Index {
#[must_use]
pub fn new(data: &Wtf8, char_len: usize) -> Self {
let mut groups = vec![
Group {
base: 0,
ofs: [0; 16],
};
char_len / 64 + 1
];
let mut remaining = char_len as isize;
let mut base = 0;
let mut current = 0;
loop {
groups[current].base = base;
let mut next = base;
let mut group_filled = true;
for i in 0..16 {
next = if remaining == 0 {
next + 1
} else {
next_pos(data, next)
};
groups[current].ofs[i] = (next - base) as u8;
remaining -= 4;
if remaining < 0 {
debug_assert_eq!(current + 1, groups.len());
group_filled = false;
break;
}
next = next_pos(data, next_pos(data, next_pos(data, next)));
}
if !group_filled {
break;
}
current += 1;
base = next;
}
Self {
groups: groups.into_boxed_slice(),
}
}
#[inline]
#[must_use]
pub fn byte_offset(&self, data: &Wtf8, index: usize) -> usize {
let group = &self.groups[index >> 6];
let pos = group.base + group.ofs[(index >> 2) & 0x0F] as usize;
match index & 0x3 {
0 => prev_pos(data, pos),
1 => pos,
2 => next_pos(data, pos),
_ => next_pos(data, next_pos(data, pos)),
}
}
#[must_use]
pub fn char_index_at_byte(&self, data: &Wtf8, bytepos: usize, char_len: usize) -> usize {
let bytes_remaining = data.len() - bytepos;
let mut group_min =
usize::max(bytepos / 4, char_len.saturating_sub(bytes_remaining + 1)) >> 6;
let mut group_max = usize::min(bytepos, char_len.saturating_sub(bytes_remaining / 4)) >> 6;
while group_min < group_max {
let middle = group_min.midpoint(group_max) + 1;
if bytepos < self.groups[middle].base {
group_max = middle - 1;
} else {
group_min = middle;
}
}
let base = self.groups[group_min].base;
if base == bytepos {
return group_min << 6;
}
let entries = if group_min == self.groups.len() - 1 {
((char_len - 1) >> 2) & 0x0F
} else {
16
};
let mut index = group_min << 6;
let mut pos = base;
for (i, &entry) in self.groups[group_min].ofs.iter().enumerate().take(entries) {
let at = base + entry as usize;
if at >= bytepos {
break;
}
pos = at;
index = (group_min << 6) + (i << 2) + 1;
}
while pos < bytepos {
pos = next_pos(data, pos);
index += 1;
}
index
}
#[must_use]
pub fn byte_size(&self) -> usize {
core::mem::size_of_val(&*self.groups)
}
}
#[inline]
fn next_pos(data: &Wtf8, pos: usize) -> usize {
match data.as_bytes()[pos] {
0x00..=0x7F => pos + 1,
0x80..=0xDF => pos + 2,
0xE0..=0xEF => pos + 3,
_ => pos + 4,
}
}
#[inline]
fn prev_pos(data: &Wtf8, pos: usize) -> usize {
let data = data.as_bytes();
let mut pos = pos - 1;
if pos >= data.len() || data[pos] <= 0x7F {
return pos;
}
pos -= 1;
if data[pos] >= 0xC0 {
return pos;
}
pos -= 1;
if data[pos] >= 0xC0 {
return pos;
}
pos - 1
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wtf8::{CodePoint, Wtf8Buf};
fn check(s: &Wtf8) {
let expected: Vec<usize> = s
.code_point_indices()
.map(|(byte_offset, _)| byte_offset)
.collect();
let char_len = expected.len();
let index = Wtf8Index::new(s, char_len);
for (i, &want) in expected.iter().enumerate() {
assert_eq!(
index.byte_offset(s, i),
want,
"index {i} of {s:?} ({char_len} code points)"
);
assert_eq!(
index.char_index_at_byte(s, want, char_len),
i,
"byte {want} of {s:?} ({char_len} code points)"
);
}
assert_eq!(
index.char_index_at_byte(s, s.len(), char_len),
char_len,
"end of {s:?}"
);
}
fn wtf8(s: &str) -> Wtf8Buf {
Wtf8Buf::from(s)
}
#[test]
fn empty() {
check(wtf8("").as_ref());
}
#[test]
fn widths() {
check(wtf8("abc").as_ref());
check(wtf8("\u{80}\u{7ff}").as_ref());
check(wtf8("\u{800}\u{ffff}").as_ref());
check(wtf8("\u{10000}\u{10ffff}").as_ref());
check(wtf8("a\u{80}\u{800}\u{10000}").as_ref());
}
#[test]
fn group_boundaries() {
for len in [1, 3, 4, 5, 63, 64, 65, 127, 128, 129, 255, 256, 257] {
for unit in ["a", "\u{80}", "\u{800}", "\u{10000}"] {
check(wtf8(&unit.repeat(len)).as_ref());
}
check(wtf8(&"a\u{80}\u{800}\u{10000}".repeat(len)).as_ref());
}
}
#[test]
fn lone_surrogates() {
let mut s = wtf8("a");
for cp in [0xD800, 0xDBFF, 0xDC00, 0xDFFF] {
s.push(CodePoint::from_u32(cp).unwrap());
s.push_str("b");
}
check(s.as_ref());
let mut s = wtf8("");
for i in 0..200 {
s.push(CodePoint::from_u32(0xD800 + (i % 0x400)).unwrap());
}
check(s.as_ref());
}
#[test]
fn byte_size_is_one_group_per_64_code_points() {
let s = wtf8(&"\u{10000}".repeat(200));
let index = Wtf8Index::new(s.as_ref(), 200);
assert_eq!(index.byte_size(), (200 / 64 + 1) * size_of::<Group>());
assert_eq!(size_of::<Group>(), 24);
}
}