Skip to main content

edgeparse_core/utils/
page_range.rs

1//! Page range parsing and filtering.
2//!
3//! Parses page range strings like "1-3,5,7-10" into a set of 1-based page
4//! numbers, then filters pipeline pages to keep only selected ones.
5
6use std::collections::HashSet;
7
8/// Parse a page range string into a set of 1-based page numbers.
9///
10/// Supports formats:
11/// - `"3"` — single page
12/// - `"1-5"` — range (inclusive)
13/// - `"1,3,5"` — comma-separated
14/// - `"1-3,7,10-12"` — mixed
15///
16/// Returns `None` if the string is empty or invalid.
17pub fn parse_page_range(range_str: &str, total_pages: usize) -> Option<HashSet<usize>> {
18    let trimmed = range_str.trim();
19    if trimmed.is_empty() {
20        return None;
21    }
22
23    let mut pages = HashSet::new();
24    for part in trimmed.split(',') {
25        let part = part.trim();
26        if part.is_empty() {
27            continue;
28        }
29        if let Some((start_str, end_str)) = part.split_once('-') {
30            let start: usize = start_str.trim().parse().ok()?;
31            let end: usize = end_str.trim().parse().ok()?;
32            if start == 0 || end == 0 || start > end {
33                return None;
34            }
35            for p in start..=end.min(total_pages) {
36                pages.insert(p);
37            }
38        } else {
39            let p: usize = part.parse().ok()?;
40            if p == 0 {
41                return None;
42            }
43            if p <= total_pages {
44                pages.insert(p);
45            }
46        }
47    }
48
49    if pages.is_empty() {
50        None
51    } else {
52        Some(pages)
53    }
54}
55
56/// Filter pages to keep only those in the selected set.
57/// `selected` contains 1-based page numbers.
58/// Returns a new Vec with only the selected pages (preserving order).
59pub fn filter_pages<T>(pages: Vec<Vec<T>>, selected: &HashSet<usize>) -> Vec<Vec<T>> {
60    pages
61        .into_iter()
62        .enumerate()
63        .filter_map(|(i, page)| {
64            let page_num = i + 1; // 1-based
65            if selected.contains(&page_num) {
66                Some(page)
67            } else {
68                None
69            }
70        })
71        .collect()
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_parse_single_page() {
80        let result = parse_page_range("3", 10).unwrap();
81        assert_eq!(result, HashSet::from([3]));
82    }
83
84    #[test]
85    fn test_parse_range() {
86        let result = parse_page_range("2-5", 10).unwrap();
87        assert_eq!(result, HashSet::from([2, 3, 4, 5]));
88    }
89
90    #[test]
91    fn test_parse_mixed() {
92        let result = parse_page_range("1-3,7,10-12", 15).unwrap();
93        assert_eq!(result, HashSet::from([1, 2, 3, 7, 10, 11, 12]));
94    }
95
96    #[test]
97    fn test_parse_empty() {
98        assert!(parse_page_range("", 10).is_none());
99    }
100
101    #[test]
102    fn test_parse_beyond_total() {
103        let result = parse_page_range("1-5", 3).unwrap();
104        assert_eq!(result, HashSet::from([1, 2, 3]));
105    }
106
107    #[test]
108    fn test_filter_pages() {
109        let pages = vec![vec![1], vec![2], vec![3], vec![4], vec![5]];
110        let selected = HashSet::from([1, 3, 5]);
111        let filtered = filter_pages(pages, &selected);
112        assert_eq!(filtered, vec![vec![1], vec![3], vec![5]]);
113    }
114}