lmocr 0.1.2

Convert PDFs and images to Markdown using OpenRouter multimodal LLMs
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());
    }
}