#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LineRange {
start: usize,
end: usize,
}
impl LineRange {
fn new(start: usize, end: usize) -> Result<Self, LineRangeParseError> {
if start == 0 || end == 0 || end < start {
return Err(LineRangeParseError::NonPositiveOrReversed);
}
Ok(Self { start, end })
}
pub(crate) fn start(self) -> usize {
self.start
}
pub(crate) fn end(self) -> usize {
self.end
}
pub(crate) fn line_count(self) -> usize {
self.end - self.start + 1
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LineRangeParseError {
MissingSeparator,
InvalidStart,
InvalidEnd,
NonPositiveOrReversed,
}
pub(crate) fn parse_line_range(value: &str) -> Result<LineRange, LineRangeParseError> {
let Some((start, end)) = value.split_once(':') else {
return Err(LineRangeParseError::MissingSeparator);
};
let start = start
.parse::<usize>()
.map_err(|_| LineRangeParseError::InvalidStart)?;
let end = end
.parse::<usize>()
.map_err(|_| LineRangeParseError::InvalidEnd)?;
LineRange::new(start, end)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ReadPlan {
range: LineRange,
total_lines: usize,
}
impl ReadPlan {
pub(crate) fn start_line(self) -> usize {
self.range.start()
}
pub(crate) fn end_line(self) -> usize {
self.range.end()
}
pub(crate) fn requested_start_line(self) -> usize {
self.range.start()
}
pub(crate) fn requested_end_line(self) -> usize {
self.range.end()
}
pub(crate) fn line_count(self) -> usize {
self.range.line_count()
}
pub(crate) fn total_lines(self) -> usize {
self.total_lines
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReadPlanError {
RangeTooLarge {
max_lines: usize,
},
RangeOutsideFile {
requested_start_line: usize,
requested_end_line: usize,
total_lines: usize,
},
}
pub(crate) fn plan_read(
range: LineRange,
total_lines: usize,
max_lines: usize,
) -> Result<ReadPlan, ReadPlanError> {
if range.line_count() > max_lines {
return Err(ReadPlanError::RangeTooLarge { max_lines });
}
if total_lines == 0 || range.start() > total_lines || range.end() > total_lines {
return Err(ReadPlanError::RangeOutsideFile {
requested_start_line: range.start(),
requested_end_line: range.end(),
total_lines,
});
}
Ok(ReadPlan { range, total_lines })
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ReadLineSlice {
pub(crate) line: usize,
pub(crate) content: String,
}
pub(crate) fn collect_read_lines(source: &str, plan: ReadPlan) -> Vec<ReadLineSlice> {
source
.lines()
.enumerate()
.skip(plan.start_line() - 1)
.take(plan.line_count())
.map(|(index, content)| ReadLineSlice {
line: index + 1,
content: content.to_string(),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_line_range_accepts_positive_ordered_bounds() {
let range = parse_line_range("2:4").unwrap();
assert_eq!(range.start(), 2);
assert_eq!(range.end(), 4);
assert_eq!(range.line_count(), 3);
}
#[test]
fn parse_line_range_rejects_invalid_shapes() {
assert_eq!(
parse_line_range("2").unwrap_err(),
LineRangeParseError::MissingSeparator
);
assert_eq!(
parse_line_range("x:2").unwrap_err(),
LineRangeParseError::InvalidStart
);
assert_eq!(
parse_line_range("1:y").unwrap_err(),
LineRangeParseError::InvalidEnd
);
assert_eq!(
parse_line_range("0:2").unwrap_err(),
LineRangeParseError::NonPositiveOrReversed
);
assert_eq!(
parse_line_range("3:2").unwrap_err(),
LineRangeParseError::NonPositiveOrReversed
);
}
#[test]
fn plan_read_rejects_ranges_larger_than_policy_before_file_bounds() {
let range = parse_line_range("1:5").unwrap();
assert_eq!(
plan_read(range, 2, 3).unwrap_err(),
ReadPlanError::RangeTooLarge { max_lines: 3 }
);
}
#[test]
fn plan_read_rejects_ranges_outside_file() {
let range = parse_line_range("2:4").unwrap();
assert_eq!(
plan_read(range, 3, 10).unwrap_err(),
ReadPlanError::RangeOutsideFile {
requested_start_line: 2,
requested_end_line: 4,
total_lines: 3,
}
);
}
#[test]
fn collect_read_lines_returns_the_planned_slice() {
let range = parse_line_range("2:3").unwrap();
let plan = plan_read(range, 4, 10).unwrap();
let lines = collect_read_lines("one\ntwo\nthree\nfour\n", plan);
assert_eq!(
lines,
vec![
ReadLineSlice {
line: 2,
content: "two".to_string(),
},
ReadLineSlice {
line: 3,
content: "three".to_string(),
},
]
);
}
}