pub struct LineLookup {
line_starts: Vec<usize>,
}
impl LineLookup {
#[must_use]
pub fn new(source: &str) -> Self {
let line_starts: Vec<usize> = std::iter::once(0)
.chain(source.match_indices('\n').map(|(i, _)| i + 1))
.collect();
Self { line_starts }
}
#[must_use]
pub fn byte_to_line(&self, byte: usize) -> u32 {
self.line_starts.partition_point(|&start| start <= byte) as u32
}
#[must_use]
pub fn byte_to_line_col(&self, byte: usize) -> (u32, u32) {
let line = self
.line_starts
.partition_point(|&start| start <= byte)
.max(1);
let line_start = self.line_starts.get(line - 1).copied().unwrap_or(0);
(line as u32, (byte.saturating_sub(line_start) as u32) + 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_byte_to_line_simple() {
let source = "line1\nline2\nline3\n";
let lookup = LineLookup::new(source);
assert_eq!(lookup.byte_to_line(0), 1);
assert_eq!(lookup.byte_to_line(4), 1);
assert_eq!(lookup.byte_to_line(5), 1);
assert_eq!(lookup.byte_to_line(6), 2);
assert_eq!(lookup.byte_to_line(10), 2);
assert_eq!(lookup.byte_to_line(12), 3);
}
#[test]
fn test_byte_to_line_empty() {
let lookup = LineLookup::new("");
assert_eq!(lookup.byte_to_line(0), 1);
}
#[test]
fn test_byte_to_line_col() {
let source = "ab\ncde\n"; let lookup = LineLookup::new(source);
assert_eq!(lookup.byte_to_line_col(0), (1, 1)); assert_eq!(lookup.byte_to_line_col(1), (1, 2)); assert_eq!(lookup.byte_to_line_col(2), (1, 3)); assert_eq!(lookup.byte_to_line_col(3), (2, 1)); assert_eq!(lookup.byte_to_line_col(5), (2, 3)); assert_eq!(lookup.byte_to_line_col(7), (3, 1));
assert_eq!(LineLookup::new("").byte_to_line_col(0), (1, 1));
}
#[test]
fn test_byte_to_line_no_trailing_newline() {
let source = "line1\nline2";
let lookup = LineLookup::new(source);
assert_eq!(lookup.byte_to_line(0), 1);
assert_eq!(lookup.byte_to_line(6), 2);
assert_eq!(lookup.byte_to_line(10), 2);
}
}