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 if valid_years.binary_search(&year).is_ok() {
10 Some(year)
11 } else {
12 None
13 }
14}
15
16pub fn parse_year_str(s: &str, valid_years: &[u32]) -> Result<Vec<u32>> {
34 assert!(
36 is_sorted_and_unique(valid_years),
37 "`valid_years` must be sorted and unique"
38 );
39
40 let s = s.trim();
41 ensure!(!s.is_empty(), "No years provided");
42
43 if s.eq_ignore_ascii_case("all") {
44 return Ok(Vec::from_iter(valid_years.iter().copied()));
45 }
46
47 ensure!(
48 !(s.contains(';') && s.contains("..")),
49 "Both ';' and '..' found in year string {s}. Discrete years and ranges cannot be mixed."
50 );
51
52 let years: Vec<_> = if s.contains("..") {
54 parse_years_range(s, valid_years)?
55 } else {
56 s.split(';')
57 .map(|y| {
58 parse_and_validate_year(y, valid_years)
59 .with_context(|| format!("Invalid year: {y}"))
60 })
61 .try_collect()?
62 };
63
64 ensure!(
65 is_sorted_and_unique(&years),
66 "Years must be in order and unique"
67 );
68
69 Ok(years)
70}
71
72fn parse_years_range(s: &str, valid_years: &[u32]) -> Result<Vec<u32>> {
78 let parts: Vec<&str> = s.split("..").collect();
80 ensure!(
81 parts.len() == 2,
82 "Year range must be of the form 'start..end', 'start..' or '..end'. Invalid: {s}"
83 );
84 let left = parts[0].trim();
85 let right = parts[1].trim();
86
87 let start = if left.is_empty() {
89 valid_years[0]
90 } else {
91 left.parse::<u32>()
92 .ok()
93 .with_context(|| format!("Invalid start year in range: {left}"))?
94 };
95
96 let end = if right.is_empty() {
98 *valid_years.last().unwrap()
99 } else {
100 right
101 .parse::<u32>()
102 .ok()
103 .with_context(|| format!("Invalid end year in range: {right}"))?
104 };
105
106 ensure!(
107 end > start,
108 "End year must be biger than start year in range {s}"
109 );
110 let years: Vec<_> = (start..=end).filter(|y| valid_years.contains(y)).collect();
111 ensure!(
112 !years.is_empty(),
113 "No valid years found in year range string {s}"
114 );
115 Ok(years)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::fixture::assert_error;
122 use rstest::rstest;
123
124 #[rstest]
125 #[case("2020", &[2020, 2021], &[2020])]
126 #[case("all", &[2020, 2021], &[2020,2021])]
127 #[case("ALL", &[2020, 2021], &[2020,2021])]
128 #[case(" ALL ", &[2020, 2021], &[2020,2021])]
129 #[case("2020;2021", &[2020, 2021], &[2020,2021])]
130 #[case(" 2020; 2021", &[2020, 2021], &[2020,2021])] #[case("2019..2026", &[2020,2025], &[2020,2025])]
132 #[case("..2023", &[2020,2025], &[2020])] #[case("2021..", &[2020,2025], &[2025])] #[case("..", &[2020,2025], &[2020,2025])]
135 fn test_parse_year_str_valid(
136 #[case] input: &str,
137 #[case] milestone_years: &[u32],
138 #[case] expected: &[u32],
139 ) {
140 assert_eq!(parse_year_str(input, milestone_years).unwrap(), expected);
141 }
142
143 #[rstest]
144 #[case("", &[2020], "No years provided")]
145 #[case("2021", &[2020], "Invalid year: 2021")]
146 #[case("a;2020", &[2020], "Invalid year: a")]
147 #[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.")]
150 #[case("2021..2020", &[2020, 2021],"End year must be biger than start year in range 2021..2020")] #[case("2021..2024", &[2020,2025], "No valid years found in year range string 2021..2024")]
152 #[case("..2020..2025", &[2020,2025], "Year range must be of the form 'start..end', 'start..' or '..end'. Invalid: ..2020..2025")]
153 #[case("2020...2025", &[2020,2025], "Invalid end year in range: .2025")]
154 fn test_parse_year_str_invalid(
155 #[case] input: &str,
156 #[case] milestone_years: &[u32],
157 #[case] error_msg: &str,
158 ) {
159 assert_error!(parse_year_str(input, milestone_years), error_msg);
160 }
161}