use pdfrum_common::PageIndex;
use crate::error::Error;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PageRange(Vec<PageIndex>);
impl PageRange {
#[must_use]
pub fn all(count: u32) -> Self {
Self((0..count).map(PageIndex::from).collect())
}
#[must_use]
pub fn of(indices: impl IntoIterator<Item = impl Into<PageIndex>>) -> Self {
Self(indices.into_iter().map(Into::into).collect())
}
pub fn parse(text: &str, count: u32) -> Result<Self, Error> {
if !text
.bytes()
.all(|b| b == b' ' || b.is_ascii_digit() || b == b'-' || b == b',')
{
return Err(Error::BadPageRange);
}
let stripped: String = text.chars().filter(|c| *c != ' ').collect();
if stripped.is_empty() {
return Ok(Self(Vec::new()));
}
let mut out = Vec::new();
for entry in stripped.split(',') {
let mut parts = entry.split('-');
let first = parts.next().unwrap_or_default();
let second = parts.next();
if parts.next().is_some() {
return Err(Error::BadPageRange);
}
match second {
None => {
let n = number(first);
if n == 0 || n > count {
return Err(Error::BadPageRange);
}
out.push(PageIndex::new(n - 1));
}
Some(second) => {
let (a, b) = (number(first), number(second));
if a == 0 || b == 0 || a > b || b > count {
return Err(Error::BadPageRange);
}
out.extend(((a - 1)..b).map(PageIndex::from));
}
}
}
Ok(Self(out))
}
#[must_use]
pub fn indices(&self) -> &[PageIndex] {
&self.0
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
fn number(text: &str) -> u32 {
let mut out: u32 = 0;
for b in text.bytes() {
let Some(digit) = (b as char).to_digit(10) else {
return 0;
};
out = out.saturating_mul(10).saturating_add(digit);
}
out
}
#[cfg(test)]
mod tests {
use pdfrum_common::PageIndex;
use super::PageRange;
fn parse(text: &str, count: u32) -> Option<Vec<u32>> {
PageRange::parse(text, count)
.ok()
.map(|r| r.indices().iter().map(|p| p.get()).collect())
}
#[test]
fn simple_ranges_expand_inclusively() {
assert_eq!(parse("1", 10), Some(vec![0]));
assert_eq!(parse("1-1", 10), Some(vec![0]));
assert_eq!(parse("1-4", 10), Some(vec![0, 1, 2, 3]));
assert_eq!(parse("1,3-5", 10), Some(vec![0, 2, 3, 4]));
assert_eq!(parse("10", 10), Some(vec![9]));
}
#[test]
fn spaces_are_stripped_from_inside_numbers() {
assert_eq!(parse("1- 4", 4), Some(vec![0, 1, 2, 3]));
assert_eq!(parse("1 -4", 4), Some(vec![0, 1, 2, 3]));
assert_eq!(parse(" 1 - 4 ", 4), Some(vec![0, 1, 2, 3]));
assert_eq!(parse("5 0, 1-2 ", 100), Some(vec![49, 0, 1]));
}
#[test]
fn duplicates_and_order_are_preserved() {
assert_eq!(parse("1-4,3-6", 10), Some(vec![0, 1, 2, 3, 2, 3, 4, 5]));
assert_eq!(parse("2,1", 10), Some(vec![1, 0]));
assert_eq!(parse("1,1,1,1", 10), Some(vec![0, 0, 0, 0]));
}
#[test]
fn the_grammar_rejects() {
for bad in [
"clams", "0", "42", "1-2-", "1-2-3",
",1", "1,",
",,",
"1-", "-1", "-,0,,,1-",
"1-2,,,,3-4",
"4-1", "1-5", "1;2", "1.2",
"a",
] {
assert_eq!(parse(bad, 4), None, "{bad:?} must fail");
}
}
#[test]
fn one_bad_entry_discards_the_whole_string() {
assert_eq!(parse("1,2,clams", 10), None);
assert_eq!(parse("1,2,99", 10), None);
}
#[test]
fn an_empty_string_names_no_pages() {
assert_eq!(parse("", 10), Some(vec![]));
assert_eq!(parse(" ", 10), Some(vec![]));
}
#[test]
fn a_one_page_document_accepts_only_page_one() {
assert_eq!(parse("1", 1), Some(vec![0]));
assert_eq!(parse("1-1", 1), Some(vec![0]));
assert_eq!(parse("2", 1), None);
assert_eq!(parse("1-2", 1), None);
}
#[test]
fn all_names_every_page_in_order() {
assert_eq!(
PageRange::all(3).indices(),
&[PageIndex::new(0), PageIndex::new(1), PageIndex::new(2)]
);
assert!(PageRange::all(0).is_empty());
}
#[test]
fn a_huge_number_saturates_rather_than_wrapping() {
assert_eq!(parse("99999999999999", 10), None);
}
}