#[inline]
pub fn line_units(line: &str) -> u64 {
line.chars().filter(|c| !c.is_whitespace()).count() as u64
}
pub fn offset_of_line<F: Fn(usize) -> bool>(
lines: &[String],
is_image: F,
start: usize,
line: usize,
) -> u64 {
let end = line.min(lines.len());
let start = start.min(end);
lines[start..end]
.iter()
.enumerate()
.map(|(off, l)| if is_image(start + off) { 0 } else { line_units(l) })
.sum()
}
pub fn line_for_offset<F: Fn(usize) -> bool>(
lines: &[String],
is_image: F,
start: usize,
end: usize,
target: u64,
) -> usize {
let end = end.min(lines.len());
let start = start.min(end);
let mut acc = 0u64;
for (off, l) in lines[start..end].iter().enumerate() {
let units = if is_image(start + off) { 0 } else { line_units(l) };
if acc + units > target {
return start + off;
}
acc += units;
}
end.saturating_sub(1).max(start)
}
pub fn total_units<F: Fn(usize) -> bool>(lines: &[String], is_image: F) -> u64 {
offset_of_line(lines, is_image, 0, lines.len())
}
pub fn fraction_of_line<F: Fn(usize) -> bool + Copy>(
lines: &[String],
is_image: F,
line: usize,
) -> f64 {
let total = total_units(lines, is_image);
if total == 0 {
return 0.0;
}
(offset_of_line(lines, is_image, 0, line) as f64 / total as f64)
.clamp(0.0, 1.0)
}
pub fn line_for_fraction<F: Fn(usize) -> bool + Copy>(
lines: &[String],
is_image: F,
frac: f64,
) -> usize {
let total = total_units(lines, is_image);
let target = (frac.clamp(0.0, 1.0) * total as f64).round() as u64;
line_for_offset(lines, is_image, 0, lines.len(), target)
}
#[cfg(test)]
mod tests {
use super::*;
fn lines(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
fn is_image(i: usize) -> bool {
i == 2
}
#[test]
fn counts_non_whitespace_skipping_image_rows() {
let l = lines(&["one two", "three", "\u{2580}\u{2580}\u{2580}", "four"]);
assert_eq!(offset_of_line(&l, is_image, 0, 4), 15);
assert_eq!(offset_of_line(&l, is_image, 0, 2), 11);
assert_eq!(offset_of_line(&l, is_image, 2, 3), 0);
}
#[test]
fn ignores_whitespace_so_padding_and_indent_do_not_shift_the_anchor() {
let narrow = lines(&["hello", "world", "again"]);
let wide = lines(&["hello world", " again"]);
let none = |_: usize| false;
assert_eq!(
offset_of_line(&narrow, none, 0, narrow.len()),
offset_of_line(&wide, none, 0, wide.len()),
);
}
#[test]
fn hard_split_token_keeps_the_same_total() {
let whole = lines(&["supercalifragilistic"]);
let split = lines(&["supercalif", "ragilistic"]);
let none = |_: usize| false;
assert_eq!(
offset_of_line(&whole, none, 0, 1),
offset_of_line(&split, none, 0, 2),
);
}
#[test]
fn line_for_offset_is_the_inverse_and_returns_absolute_index() {
let l = lines(&["one two", "three", "\u{2580}\u{2580}\u{2580}", "four"]);
assert_eq!(line_for_offset(&l, is_image, 0, 4, 0), 0);
assert_eq!(line_for_offset(&l, is_image, 0, 4, 6), 1);
assert_eq!(line_for_offset(&l, is_image, 0, 4, 11), 3);
assert_eq!(line_for_offset(&l, is_image, 0, 4, 9_999), 3);
}
#[test]
fn fraction_is_width_independent() {
let narrow = lines(&["ab", "cd", "ef", "gh"]);
let wide = lines(&["ab cd", "ef gh"]);
let none = |_: usize| false;
assert!((fraction_of_line(&narrow, none, 2) - 0.5).abs() < 1e-9);
assert!((fraction_of_line(&wide, none, 1) - 0.5).abs() < 1e-9);
}
#[test]
fn fraction_and_line_for_fraction_round_trip() {
let l = lines(&["one two", "three", "four", "five six"]);
let none = |_: usize| false;
for line in 0..l.len() {
let frac = fraction_of_line(&l, none, line);
assert_eq!(line_for_fraction(&l, none, frac), line);
}
}
#[test]
fn empty_document_reads_zero() {
let none = |_: usize| false;
assert_eq!(fraction_of_line(&[], none, 0), 0.0);
}
#[test]
fn page_local_round_trip_within_a_page() {
let l = lines(&["cover", "alpha beta", "gamma", "delta"]);
let none = |_: usize| false;
for line in 1..4 {
let off = offset_of_line(&l, none, 1, line);
assert_eq!(line_for_offset(&l, none, 1, 4, off), line);
}
}
}