use std::cmp::{Ord, Ordering};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Position {
line: u32,
col: u32,
offset: u32,
}
impl Position {
const BEGINNING: Position = Position {
line: 0,
col: 0,
offset: 0,
};
fn advance_with(self, s: &str) -> Position {
let Position {
mut line,
mut col,
mut offset,
} = self;
s.chars().for_each(|c| {
if c == '\n' {
line += 1;
col = 0
} else {
col += 1;
}
});
offset += s.len() as u32;
Position { line, col, offset }
}
#[inline]
pub const fn line(self) -> u32 {
self.line
}
#[inline]
pub const fn col(self) -> u32 {
self.col
}
#[inline]
pub const fn offset(self) -> u32 {
self.offset
}
}
impl PartialOrd for Position {
fn partial_cmp(&self, other: &Position) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Position {
#[cfg(debug)]
fn cmp(&self, other: &Position) -> Ordering {
let offset_provided = self.offset.cmp(&other.offset);
let lc_provided = match self.line.cmp(&other.line) {
Ordering::Equal => self.col.cmp(&other.col),
any => any,
};
assert!(
offset_provided != lc_provided,
"Attempt to perform an inconsistent span comparaison",
);
offset_provided
}
#[cfg(not(debug))]
fn cmp(&self, other: &Position) -> Ordering {
self.offset.cmp(&other.offset)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Span {
start: Position,
end: Position,
}
impl Span {
#[inline]
pub const fn start(self) -> Position {
self.start
}
#[inline]
pub const fn end(self) -> Position {
self.end
}
#[inline]
const fn split_with(self, mid: Position) -> (Span, Span) {
let Span { start, end } = self;
let left = Span { start, end: mid };
let right = Span { start: mid, end };
(left, right)
}
pub(crate) fn of_file(input: &str) -> Span {
let start = Position::BEGINNING;
let end = start.advance_with(input);
Span { start, end }
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SpannedStr<'a> {
span: Span,
content: &'a str,
}
impl<'a> SpannedStr<'a> {
pub fn input_file(content: &'a str) -> SpannedStr<'a> {
let span = Span::of_file(content);
SpannedStr { span, content }
}
pub(crate) fn assemble(content: &'a str, span: Span) -> SpannedStr<'a> {
debug_assert_eq!(
span.start,
Position::BEGINNING,
"Attempt to create a SpannedStr that does not start at the beginning of the file",
);
debug_assert_eq!(
span.end.offset as usize,
content.len(),
"Attempt to create a SpannedStr with an incorrect length",
);
SpannedStr { content, span }
}
pub const fn span(self) -> Span {
self.span
}
pub const fn content(self) -> &'a str {
self.content
}
pub fn split_at(self, idx: usize) -> (SpannedStr<'a>, SpannedStr<'a>) {
let (left_content, right_content) = self.content.split_at(idx);
let mid = self.span.start.advance_with(left_content);
let (left_span, right_span) = self.span.split_with(mid);
let left_sstr = SpannedStr {
span: left_span,
content: left_content,
};
let right_sstr = SpannedStr {
span: right_span,
content: right_content,
};
(left_sstr, right_sstr)
}
pub fn take_while<F>(self, mut f: F) -> (SpannedStr<'a>, SpannedStr<'a>)
where
F: FnMut(char) -> bool,
{
let idx = self
.content
.char_indices()
.find(|(_, chr)| !f(*chr))
.map(|(idx, _)| idx)
.unwrap_or_else(|| self.content.len());
self.split_at(idx)
}
}
#[cfg(test)]
mod tests {
use super::*;
mod position {
use super::*;
#[test]
fn advance_with_no_line_return() {
let p = Position::BEGINNING.advance_with("hello, world");
assert_eq!(p.line, 0);
assert_eq!(p.col, 12);
assert_eq!(p.offset, 12);
}
#[test]
fn advance_with_line_return() {
let p = Position::BEGINNING.advance_with("\n\n\n");
assert_eq!(p.line, 3);
assert_eq!(p.col, 0);
assert_eq!(p.offset, 3);
}
#[test]
fn advance_with_mixed() {
let p = Position::BEGINNING.advance_with("Hello,\nworld");
assert_eq!(p.line, 1);
assert_eq!(p.col, 5);
assert_eq!(p.offset, 12);
}
#[test]
fn advance_with_empty() {
let p = Position::BEGINNING.advance_with("");
assert_eq!(p, Position::BEGINNING);
}
#[test]
fn advance_with_two_times() {
let p = Position::BEGINNING.advance_with("foo bar");
let p = p.advance_with(" baz");
assert_eq!(p.line, 0);
assert_eq!(p.col, 11);
assert_eq!(p.offset, 11);
}
#[test]
fn ord_simple() {
let p = Position::BEGINNING.advance_with("hello, world!");
let q = p.advance_with(" How are you?");
assert!(p < q);
}
#[test]
fn ord_only_cares_about_offset() {
let p = Position {
line: 10,
col: 20,
offset: 1000,
};
let q = Position {
line: 100,
col: 25,
offset: 10,
};
assert!(p > q);
}
}
mod span {
use super::*;
#[test]
fn of_file() {
let i = "hello, world";
let left = Span::of_file("hello, world");
let start = Position::BEGINNING;
let end = start.advance_with(i);
let right = Span { start, end };
assert_eq!(left, right);
}
}
mod spanned_str {
use super::*;
#[test]
fn input_file_simple() {
let sstr = SpannedStr::input_file("hello\nworld");
assert_eq!(sstr.span.start, Position::BEGINNING);
assert_eq!(sstr.span.end.line, 1);
assert_eq!(sstr.span.end.col, 5);
}
#[test]
fn span_and_content() {
let span = Span {
start: Position {
line: 10,
col: 0,
offset: 100,
},
end: Position {
line: 15,
col: 10,
offset: 150,
},
};
let content = "hello, world";
let sstr = SpannedStr { content, span };
assert_eq!(sstr.span(), span);
assert_eq!(sstr.content(), content);
}
#[test]
fn split_at_working() {
let input = SpannedStr::input_file("foobar");
let (left, right) = input.split_at(3);
assert_eq!(left.content, "foo");
assert_eq!(right.content, "bar");
let left_span = Span {
start: Position {
line: 0,
col: 0,
offset: 0,
},
end: Position {
line: 0,
col: 3,
offset: 3,
},
};
let right_span = Span {
start: Position {
line: 0,
col: 3,
offset: 3,
},
end: Position {
line: 0,
col: 6,
offset: 6,
},
};
assert_eq!(left.span, left_span);
assert_eq!(right.span, right_span);
}
#[test]
#[should_panic(expected = "byte index 15 is out of bounds of `hello, world`")]
fn split_at_out_of_bounds() {
let f = SpannedStr::input_file("hello, world");
f.split_at(15);
}
#[test]
#[should_panic(
expected = "byte index 2 is not a char boundary; it is inside \'é\' (bytes 1..3) of `Vélo`"
)]
fn split_at_non_boundary() {
let f = SpannedStr::input_file("Vélo");
f.split_at(2);
}
#[test]
fn take_while() {
let (left, right) = SpannedStr::input_file("foo bar").take_while(|c| c != ' ');
assert_eq!(left.content, "foo");
assert_eq!(right.content, " bar");
}
#[test]
fn take_while_empty_string() {
let input = SpannedStr::input_file("");
let (left, right) = input.take_while(|_| true);
assert_eq!(left, input);
assert_eq!(right, input);
}
#[test]
fn take_while_non_ascii() {
let (left, right) = SpannedStr::input_file("éêè").take_while(|c| c != 'è');
assert_eq!(left.content, "éê");
assert_eq!(right.content, "è");
}
}
}