use std::num::NonZeroUsize;
#[derive(Debug)]
pub struct LinesWithOffsets<'a> {
remaining: &'a str,
current_offset: usize,
line_no: NonZeroUsize,
}
impl<'a> LinesWithOffsets<'a> {
fn new(input: &'a str) -> Self {
LinesWithOffsets {
remaining: input,
current_offset: 0,
line_no: NonZeroUsize::try_from(1).unwrap(),
}
}
}
impl<'a> Iterator for LinesWithOffsets<'a> {
type Item = (NonZeroUsize, usize, usize, &'a str);
fn next(&mut self) -> Option<Self::Item> {
if self.remaining.is_empty() {
return None;
}
let (line_end, next_offset) = match self.remaining.find(|c| c == '\n' || c == '\r') {
Some(idx) => {
let next_offset = idx
+ if self.remaining.as_bytes().get(idx) == Some(&b'\r')
&& self.remaining.as_bytes().get(idx + 1) == Some(&b'\n')
{
2
} else {
1
};
(idx, next_offset)
}
None => (self.remaining.len(), self.remaining.len()),
};
let line = &self.remaining[..line_end];
let item = (
self.line_no,
self.current_offset,
self.current_offset + line_end,
line,
);
self.remaining = if next_offset < self.remaining.len() {
&self.remaining[next_offset..]
} else {
""
};
self.current_offset += next_offset;
self.line_no = match self.line_no.checked_add(1) {
Some(line_number) => line_number,
None => {
panic!("LinesWithOffsets does not support {} lines", usize::MAX)
}
};
Some(item)
}
}
pub trait LinesWithOffsetsExt {
fn lines_with_offsets(&self) -> LinesWithOffsets<'_>;
}
impl LinesWithOffsetsExt for str {
fn lines_with_offsets(&self) -> LinesWithOffsets<'_> {
LinesWithOffsets::new(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lines_with_offset() {
let text = "Hello\nworld\r\nthis is a test";
let mut iter = text.lines_with_offsets();
assert_eq!(
iter.next(),
Some((NonZeroUsize::try_from(1).unwrap(), 0, 5, "Hello"))
);
assert_eq!(
iter.next(),
Some((NonZeroUsize::try_from(2).unwrap(), 6, 11, "world"))
);
assert_eq!(
iter.next(),
Some((NonZeroUsize::try_from(3).unwrap(), 13, 27, "this is a test"))
);
assert_eq!(iter.next(), None); }
}