1use super::{is_sorted_and_unique, is_sorted_and_unique_with, parse_range_parts, partition};
3use anyhow::{Context, Result, bail, ensure};
4use itertools::Itertools;
5use std::ops::RangeInclusive;
6
7fn get_valid_years_in_range(
9 range: &RangeInclusive<u32>,
10 valid_years: &[u32],
11) -> impl Iterator<Item = u32> {
12 valid_years
13 .iter()
14 .copied()
15 .filter(move |year| range.contains(year))
16}
17
18pub fn parse_year_str(s: &str, valid_years: &[u32]) -> Result<Vec<u32>> {
37 assert!(!valid_years.is_empty(), "`valid_years` cannot be empty");
38 assert!(
39 is_sorted_and_unique(valid_years),
40 "`valid_years` must be sorted and unique"
41 );
42
43 let s = s.trim();
44 ensure!(!s.is_empty(), "No years provided");
45
46 if s.eq_ignore_ascii_case("all") {
47 return Ok(Vec::from_iter(valid_years.iter().copied()));
48 }
49
50 let ranges: Vec<_> = s
52 .split(';')
53 .map(|s| {
54 let (start, end) = partition(s, "..").unwrap_or((s, s));
55 parse_range_parts(
56 start,
57 end,
58 u32::MIN..=u32::MAX,
59 *valid_years.first().unwrap(),
60 *valid_years.last().unwrap(),
61 )
62 .with_context(|| format!("Invalid year range: {s}"))
63 })
64 .try_collect()?;
65
66 ensure!(
67 is_sorted_and_unique_with(ranges.iter(), |a, b| {
68 a.start() < b.start() && a.end() < b.start()
69 }),
70 "Year ranges must be sorted and non-overlapping"
71 );
72
73 let mut years = Vec::new();
74 for range in ranges {
75 let old_len = years.len();
76 years.extend(get_valid_years_in_range(&range, valid_years));
77
78 if years.len() == old_len {
80 if range.start() == range.end() {
82 bail!("Invalid year: {}", range.start());
83 }
84 bail!(
85 "No valid years in year range: {}..{}",
86 range.start(),
87 range.end()
88 );
89 }
90 }
91
92 Ok(years)
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::fixture::assert_error;
99 use rstest::rstest;
100
101 #[rstest]
102 #[case("2020", &[2020, 2021], &[2020])]
103 #[case("all", &[2020, 2021], &[2020, 2021])]
104 #[case("ALL", &[2020, 2021], &[2020, 2021])]
105 #[case(" ALL ", &[2020, 2021], &[2020, 2021])]
106 #[case("2020;2021", &[2020, 2021], &[2020, 2021])]
107 #[case(" 2020; 2021", &[2020, 2021], &[2020, 2021])] #[case("2019..2026", &[2020, 2025], &[2020, 2025])]
109 #[case("..2023", &[2020, 2025], &[2020])] #[case("2021..", &[2020, 2025], &[2025])] #[case("2020;2021..2022", &[2020, 2021, 2022], &[2020, 2021, 2022])] fn parse_year_str_valid(
113 #[case] input: &str,
114 #[case] milestone_years: &[u32],
115 #[case] expected: &[u32],
116 ) {
117 assert_eq!(parse_year_str(input, milestone_years).unwrap(), expected);
118 }
119
120 #[rstest]
121 #[case("", &[2020], "No years provided")]
122 #[case("2021", &[2020], "Invalid year: 2021")]
123 #[case("a;2020", &[2020], "Invalid year range: a")]
124 #[case("2021;2020", &[2020, 2021], "Year ranges must be sorted and non-overlapping")] #[case("2021;2020;2021", &[2020, 2021], "Year ranges must be sorted and non-overlapping")] #[case("2021..2020", &[2020, 2021], "Invalid year range: 2021..2020")] #[case("2021..2024", &[2020, 2025], "No valid years in year range: 2021..2024")]
128 #[case("..2020..2025", &[2020, 2025], "Invalid year range: ..2020..2025")]
129 #[case("2020...2025", &[2020, 2025], "Invalid year range: 2020...2025")]
130 #[case("..", &[2020, 2025], "Invalid year range: ..")]
131 fn parse_year_str_invalid(
132 #[case] input: &str,
133 #[case] milestone_years: &[u32],
134 #[case] error_msg: &str,
135 ) {
136 assert_error!(parse_year_str(input, milestone_years), error_msg);
137 }
138}