1use crate::input::is_sorted_and_unique;
3use anyhow::{Context, Result, ensure};
4use itertools::Itertools;
5
6fn parse_and_validate_year(s: &str, valid_years: &[u32]) -> Option<u32> {
8 let year = s.trim().parse::<u32>().ok()?;
9 valid_years.binary_search(&year).is_ok().then_some(year)
10}
11
12pub fn parse_year_str(s: &str, valid_years: &[u32]) -> Result<Vec<u32>> {
30 assert!(
32 is_sorted_and_unique(valid_years),
33 "`valid_years` must be sorted and unique"
34 );
35
36 let s = s.trim();
37 ensure!(!s.is_empty(), "No years provided");
38
39 if s.eq_ignore_ascii_case("all") {
40 return Ok(Vec::from_iter(valid_years.iter().copied()));
41 }
42
43 ensure!(
44 !(s.contains(';') && s.contains("..")),
45 "Both ';' and '..' found in year string {s}. Discrete years and ranges cannot be mixed."
46 );
47
48 let years: Vec<_> = if s.contains("..") {
50 parse_years_range(s, valid_years)?
51 } else {
52 s.split(';')
53 .map(|y| {
54 parse_and_validate_year(y, valid_years)
55 .with_context(|| format!("Invalid year: {y}"))
56 })
57 .try_collect()?
58 };
59
60 ensure!(
61 is_sorted_and_unique(&years),
62 "Years must be in order and unique"
63 );
64
65 Ok(years)
66}
67
68fn parse_years_range(s: &str, valid_years: &[u32]) -> Result<Vec<u32>> {
74 let parts: Vec<&str> = s.split("..").collect();
76 ensure!(
77 parts.len() == 2,
78 "Year range must be of the form 'start..end', 'start..' or '..end'. Invalid: {s}"
79 );
80 let left = parts[0].trim();
81 let right = parts[1].trim();
82
83 let start = if left.is_empty() {
85 valid_years[0]
86 } else {
87 left.parse::<u32>()
88 .ok()
89 .with_context(|| format!("Invalid start year in range: {left}"))?
90 };
91
92 let end = if right.is_empty() {
94 *valid_years.last().unwrap()
95 } else {
96 right
97 .parse::<u32>()
98 .ok()
99 .with_context(|| format!("Invalid end year in range: {right}"))?
100 };
101
102 ensure!(
103 end > start,
104 "End year must be bigger than start year in range {s}"
105 );
106 let years: Vec<_> = (start..=end).filter(|y| valid_years.contains(y)).collect();
107 ensure!(
108 !years.is_empty(),
109 "No valid years found in year range string {s}"
110 );
111 Ok(years)
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::fixture::assert_error;
118 use rstest::rstest;
119
120 #[rstest]
121 #[case("2020", &[2020, 2021], &[2020])]
122 #[case("all", &[2020, 2021], &[2020,2021])]
123 #[case("ALL", &[2020, 2021], &[2020,2021])]
124 #[case(" ALL ", &[2020, 2021], &[2020,2021])]
125 #[case("2020;2021", &[2020, 2021], &[2020,2021])]
126 #[case(" 2020; 2021", &[2020, 2021], &[2020,2021])] #[case("2019..2026", &[2020,2025], &[2020,2025])]
128 #[case("..2023", &[2020,2025], &[2020])] #[case("2021..", &[2020,2025], &[2025])] #[case("..", &[2020,2025], &[2020,2025])]
131 fn parse_year_str_valid(
132 #[case] input: &str,
133 #[case] milestone_years: &[u32],
134 #[case] expected: &[u32],
135 ) {
136 assert_eq!(parse_year_str(input, milestone_years).unwrap(), expected);
137 }
138
139 #[rstest]
140 #[case("", &[2020], "No years provided")]
141 #[case("2021", &[2020], "Invalid year: 2021")]
142 #[case("a;2020", &[2020], "Invalid year: a")]
143 #[case("2021;2020", &[2020, 2021],"Years must be in order and unique")] #[case("2021;2020;2021", &[2020, 2021],"Years must be in order and unique")] #[case("2021;2020..2021", &[2020, 2021],"Both ';' and '..' found in year string 2021;2020..2021. Discrete years and ranges cannot be mixed.")]
146 #[case("2021..2020", &[2020, 2021],"End year must be bigger than start year in range 2021..2020")] #[case("2021..2024", &[2020,2025], "No valid years found in year range string 2021..2024")]
148 #[case("..2020..2025", &[2020,2025], "Year range must be of the form 'start..end', 'start..' or '..end'. Invalid: ..2020..2025")]
149 #[case("2020...2025", &[2020,2025], "Invalid end year in range: .2025")]
150 fn parse_year_str_invalid(
151 #[case] input: &str,
152 #[case] milestone_years: &[u32],
153 #[case] error_msg: &str,
154 ) {
155 assert_error!(parse_year_str(input, milestone_years), error_msg);
156 }
157}