use anyhow::{Context, Result};
pub fn parse_pages(spec: Option<&str>, total_pages: usize) -> Result<Vec<usize>> {
if total_pages == 0 {
anyhow::bail!("Input has zero pages.");
}
if spec.is_none() {
return Ok((1..=total_pages).collect());
}
let spec = spec.unwrap();
let mut pages = Vec::new();
for part in spec.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
if let Some((start, end)) = part.split_once('-') {
let start: usize = start
.trim()
.parse()
.with_context(|| format!("Invalid range start: {start}"))?;
let end: usize = end
.trim()
.parse()
.with_context(|| format!("Invalid range end: {end}"))?;
let (lo, hi) = if start <= end {
(start, end)
} else {
(end, start)
};
for page in lo..=hi {
pages.push(page);
}
} else {
let page: usize = part
.parse()
.with_context(|| format!("Invalid page number: {part}"))?;
pages.push(page);
}
}
pages.sort_unstable();
pages.dedup();
for page in &pages {
if *page == 0 || *page > total_pages {
anyhow::bail!(
"Page {} is out of range. Input has {} pages.",
page,
total_pages
);
}
}
Ok(pages)
}
#[cfg(test)]
mod tests {
use super::parse_pages;
#[test]
fn parse_all_pages() {
let pages = parse_pages(None, 3).unwrap();
assert_eq!(pages, vec![1, 2, 3]);
}
#[test]
fn parse_ranges() {
let pages = parse_pages(Some("1-3,5"), 10).unwrap();
assert_eq!(pages, vec![1, 2, 3, 5]);
}
#[test]
fn parse_reverse_range() {
let pages = parse_pages(Some("5-3"), 10).unwrap();
assert_eq!(pages, vec![3, 4, 5]);
}
#[test]
fn parse_single_page() {
let pages = parse_pages(Some("5"), 10).unwrap();
assert_eq!(pages, vec![5]);
}
#[test]
fn parse_deduplicates() {
let pages = parse_pages(Some("3,3,3"), 10).unwrap();
assert_eq!(pages, vec![3]);
}
#[test]
fn parse_overlapping_ranges_dedup() {
let pages = parse_pages(Some("1-3,2-4"), 10).unwrap();
assert_eq!(pages, vec![1, 2, 3, 4]);
}
#[test]
fn parse_empty_parts_skipped() {
let pages = parse_pages(Some("1,,3"), 10).unwrap();
assert_eq!(pages, vec![1, 3]);
}
#[test]
fn parse_page_zero_errors() {
assert!(parse_pages(Some("0"), 10).is_err());
}
#[test]
fn parse_page_beyond_total_errors() {
assert!(parse_pages(Some("11"), 10).is_err());
}
#[test]
fn parse_zero_total_pages_errors() {
assert!(parse_pages(None, 0).is_err());
}
#[test]
fn parse_whitespace_trimmed() {
let pages = parse_pages(Some(" 1 - 3 , 5 "), 10).unwrap();
assert_eq!(pages, vec![1, 2, 3, 5]);
}
#[test]
fn parse_invalid_number_errors() {
assert!(parse_pages(Some("abc"), 10).is_err());
}
}