tygr 0.3.0

Define your grammar once as Rust types and get a parser, printer, and EBNF presentation for free.
Documentation
//! Offset-Line-Character (OLC): converts between byte offsets (as reported by
//! [`Error::pos`](crate::Error::pos)) and `(line, character)` positions.

/// Converts between byte offsets and `(line, character)` positions in a fixed
/// input string.
pub struct OLC {
    map: Vec<usize>,
}

impl OLC {
    /// Index `input`'s line boundaries, for later offset/line-character conversions.
    pub fn new(input: &str) -> Self {
        let mut map = vec![];
        for (i, ch) in input.char_indices() {
            if ch == '\n' {
                map.push(i + 1);
            }
        }
        Self { map }
    }

    /// Convert a byte offset into a zero-indexed `(line, character)` pair.
    pub fn offset_to_line_character(&self, offset: usize) -> (usize, usize) {
        let line = self.map.partition_point(|&line_start| line_start <= offset);
        if line == 0 {
            (0, offset)
        } else {
            (line, offset - self.map[line - 1])
        }
    }

    /// Convert a zero-indexed `(line, character)` pair into a byte offset.
    pub fn line_character_to_offset(&self, (line, character): (usize, usize)) -> usize {
        if line == 0 {
            character
        } else {
            character + self.map[line - 1]
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn reference_offset_to_line_char(input: &str, offset: usize) -> (usize, usize) {
        let (mut line, mut character) = (0, 0);
        for ch in input.chars().take(offset) {
            if ch == '\n' {
                line += 1;
                character = 0;
            } else {
                character += 1;
            }
        }
        (line, character)
    }

    fn check_all_offsets(input: &str) {
        let olc = OLC::new(input);
        for offset in 0..=input.len() {
            let expected = reference_offset_to_line_char(input, offset);
            let actual = olc.offset_to_line_character(offset);
            assert_eq!(
                actual, expected,
                "mismatch at offset {offset} in {input:?}: got {actual:?}, expected {expected:?}"
            );
        }
    }

    #[test]
    fn empty() {
        check_all_offsets("");
    }

    #[test]
    fn single_line() {
        check_all_offsets("hello");
    }

    #[test]
    fn two_lines() {
        check_all_offsets("hello\nworld");
    }

    #[test]
    fn trailing_newline() {
        check_all_offsets("hello\n");
    }

    #[test]
    fn leading_newline() {
        check_all_offsets("\nhello");
    }

    #[test]
    fn consecutive_newlines() {
        check_all_offsets("a\n\nb");
    }

    #[test]
    fn only_newlines() {
        check_all_offsets("\n\n\n");
    }

    #[test]
    fn multiple_lines() {
        check_all_offsets("foo\nbar\nbaz\nqux");
    }

    #[test]
    fn single_char_lines() {
        check_all_offsets("a\nb\nc\nd");
    }
}