pub trait LimitProvider: Sync {
fn lim_at(&self, p: usize) -> usize;
#[inline]
fn boundary_order(
&self,
p_a: usize,
lim_a: usize,
p_b: usize,
lim_b: usize,
) -> std::cmp::Ordering {
let _ = (p_a, p_b);
lim_a.cmp(&lim_b)
}
}
#[derive(Copy, Clone, Debug)]
pub struct PlainText {
pub n: usize,
}
impl PlainText {
#[inline]
pub fn new(n: usize) -> Self {
Self { n }
}
}
impl LimitProvider for PlainText {
#[inline(always)]
fn lim_at(&self, p: usize) -> usize {
self.n - p
}
}
#[derive(Clone, Debug)]
pub struct SegmentedText {
n: usize,
ends: Vec<u64>,
directory: Option<BoundaryDirectory>,
}
#[derive(Clone, Debug)]
struct BoundaryDirectory {
block_shift: u32,
first_after_block_start: Vec<u32>,
}
impl BoundaryDirectory {
const MIN_ENDS: usize = 256;
const MAX_BLOCKS: usize = 2_000_000;
fn build(n: usize, ends: &[u64]) -> Option<Self> {
if ends.len() < Self::MIN_ENDS || ends.len() > u32::MAX as usize || n == 0 {
return None;
}
let target_blocks = ends.len().saturating_mul(2).clamp(1, Self::MAX_BLOCKS);
let min_block_size = n.div_ceil(target_blocks);
let block_size = min_block_size
.checked_next_power_of_two()
.unwrap_or(1usize << (usize::BITS - 1));
let block_shift = block_size.trailing_zeros();
let n_blocks = n.div_ceil(block_size);
let mut first_after_block_start = Vec::with_capacity(n_blocks + 1);
let mut end_index = 0usize;
for block in 0..=n_blocks {
let block_start = block.saturating_mul(block_size).min(n) as u64;
while end_index < ends.len() && ends[end_index] <= block_start {
end_index += 1;
}
first_after_block_start.push(end_index as u32);
}
Some(Self {
block_shift,
first_after_block_start,
})
}
}
impl SegmentedText {
pub fn from_lengths(text_len: usize, lengths: &[usize]) -> Self {
let mut ends = Vec::with_capacity(lengths.len());
let mut cum: u64 = 0;
for &len in lengths {
cum += len as u64;
ends.push(cum);
}
assert_eq!(
cum as usize, text_len,
"SegmentedText::from_lengths: per-segment lengths sum to {cum} but text_len is {text_len}",
);
let directory = BoundaryDirectory::build(text_len, &ends);
Self {
n: text_len,
ends,
directory,
}
}
pub fn from_ends(text_len: usize, ends: Vec<u64>) -> Self {
assert!(
ends.windows(2).all(|w| w[0] < w[1]),
"SegmentedText::from_ends: ends must be strictly increasing",
);
match ends.last() {
Some(&last) => assert_eq!(
last as usize, text_len,
"SegmentedText::from_ends: last end ({last}) != text_len ({text_len})",
),
None => assert_eq!(
text_len, 0,
"SegmentedText::from_ends: empty ends but text_len ({text_len}) != 0",
),
}
let directory = BoundaryDirectory::build(text_len, &ends);
Self {
n: text_len,
ends,
directory,
}
}
#[inline]
pub fn text_len(&self) -> usize {
self.n
}
#[inline]
pub fn n_segments(&self) -> usize {
self.ends.len()
}
#[inline]
pub fn ends(&self) -> &[u64] {
&self.ends
}
}
impl LimitProvider for SegmentedText {
#[inline]
fn lim_at(&self, p: usize) -> usize {
if let Some(directory) = &self.directory
&& p < self.n
{
let block = p >> directory.block_shift;
let lo = directory.first_after_block_start[block] as usize;
let mut hi = directory.first_after_block_start[block + 1] as usize;
hi = hi.max(lo + 1).min(self.ends.len());
let i = lo + self.ends[lo..hi].partition_point(|&b| b <= p as u64);
return self.ends[i] as usize - p;
}
let i = self.ends.partition_point(|&b| b <= p as u64);
if i < self.ends.len() {
self.ends[i] as usize - p
} else {
self.n - p
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_text_lim_at_matches_n_minus_p() {
let lp = PlainText::new(100);
assert_eq!(lp.lim_at(0), 100);
assert_eq!(lp.lim_at(50), 50);
assert_eq!(lp.lim_at(99), 1);
assert_eq!(lp.lim_at(100), 0);
}
#[test]
fn segmented_from_lengths_cumulates_ends() {
let lp = SegmentedText::from_lengths(15, &[3, 5, 7]);
assert_eq!(lp.n_segments(), 3);
assert_eq!(lp.ends(), &[3, 8, 15]);
}
#[test]
#[should_panic(expected = "sum to")]
fn segmented_from_lengths_rejects_undercoverage() {
let _ = SegmentedText::from_lengths(20, &[3, 5, 7]);
}
#[test]
fn segmented_lim_at_caps_at_next_boundary() {
let lp = SegmentedText::from_lengths(15, &[3, 5, 7]);
assert_eq!(lp.lim_at(0), 3);
assert_eq!(lp.lim_at(1), 2);
assert_eq!(lp.lim_at(2), 1);
assert_eq!(lp.lim_at(3), 5);
assert_eq!(lp.lim_at(5), 3);
assert_eq!(lp.lim_at(7), 1);
assert_eq!(lp.lim_at(8), 7);
assert_eq!(lp.lim_at(14), 1);
assert_eq!(lp.lim_at(15), 0);
}
#[test]
fn segmented_handles_single_segment_text() {
let lp = SegmentedText::from_lengths(10, &[10]);
assert_eq!(lp.lim_at(0), 10);
assert_eq!(lp.lim_at(5), 5);
assert_eq!(lp.lim_at(10), 0);
}
#[test]
fn segmented_directory_matches_binary_search() {
let lengths: Vec<usize> = (0..2_000).map(|i| 1 + i % 97).collect();
let n = lengths.iter().sum();
let indexed = SegmentedText::from_lengths(n, &lengths);
assert!(indexed.directory.is_some());
for p in 0..=n {
let i = indexed.ends.partition_point(|&b| b <= p as u64);
let want = if i < indexed.ends.len() {
indexed.ends[i] as usize - p
} else {
n - p
};
assert_eq!(indexed.lim_at(p), want, "p={p}");
}
}
#[test]
fn segmented_handles_empty_text() {
let lp = SegmentedText::from_lengths(0, &[]);
assert_eq!(lp.n_segments(), 0);
}
#[test]
fn segmented_from_ends_matches_from_lengths() {
let a = SegmentedText::from_lengths(15, &[3, 5, 7]);
let b = SegmentedText::from_ends(15, vec![3, 8, 15]);
assert_eq!(a.ends(), b.ends());
for p in 0..=15 {
assert_eq!(a.lim_at(p), b.lim_at(p), "p={p}");
}
}
}