1use anyhow::{Result, bail};
7
8pub fn parse(spec: &str, total: u32) -> Result<Vec<u32>> {
10 if total == 0 {
11 bail!("document has no pages");
12 }
13 let mut out = Vec::new();
14 for part in spec.split(',').map(str::trim).filter(|p| !p.is_empty()) {
15 match part.to_ascii_lowercase().as_str() {
16 "all" => out.extend(1..=total),
17 "odd" => out.extend((1..=total).step_by(2)),
18 "even" => out.extend((2..=total).step_by(2)),
19 p => match p.split_once('-') {
20 None => out.push(bound(p, total)?),
21 Some((a, b)) => {
22 let start = if a.trim().is_empty() {
23 1
24 } else {
25 bound(a, total)?
26 };
27 let end = if b.trim().is_empty() {
28 total
29 } else {
30 bound(b, total)?
31 };
32 if start <= end {
33 out.extend(start..=end);
34 } else {
35 out.extend((end..=start).rev());
36 }
37 }
38 },
39 }
40 }
41 if out.is_empty() {
42 bail!("page spec '{spec}' selects no pages");
43 }
44 Ok(out)
45}
46
47pub fn parse_or_all(spec: Option<&str>, total: u32) -> Result<Vec<u32>> {
49 match spec {
50 Some(s) => parse(s, total),
51 None => parse("all", total),
52 }
53}
54
55fn bound(s: &str, total: u32) -> Result<u32> {
56 let s = s.trim();
57 if s == "last" {
58 return Ok(total);
59 }
60 let n: u32 = s
61 .parse()
62 .map_err(|_| anyhow::anyhow!("invalid page '{s}'"))?;
63 if n == 0 || n > total {
64 bail!("page {n} out of range 1-{total}");
65 }
66 Ok(n)
67}
68
69#[cfg(test)]
70mod tests {
71 use super::parse;
72
73 #[test]
74 fn singles_ranges_and_keywords() {
75 assert_eq!(parse("1,3-5", 10).unwrap(), [1, 3, 4, 5]);
76 assert_eq!(parse("8-", 10).unwrap(), [8, 9, 10]);
77 assert_eq!(parse("-2, last", 10).unwrap(), [1, 2, 10]);
78 assert_eq!(parse("3-1", 10).unwrap(), [3, 2, 1]);
79 assert_eq!(parse("odd", 5).unwrap(), [1, 3, 5]);
80 assert_eq!(parse("even", 5).unwrap(), [2, 4]);
81 assert_eq!(parse("all", 3).unwrap(), [1, 2, 3]);
82 assert_eq!(parse("2,2", 3).unwrap(), [2, 2]);
83 }
84
85 #[test]
86 fn rejects_bad_input() {
87 assert!(parse("0", 5).is_err());
88 assert!(parse("6", 5).is_err());
89 assert!(parse("a-b", 5).is_err());
90 assert!(parse("", 5).is_err());
91 assert!(parse("even", 1).is_err());
92 assert!(parse("1", 0).is_err());
93 }
94}