]>
Commit | Line | Data |
---|---|---|
6a06907d XL |
1 | use std::{ |
2 | collections::BTreeMap, | |
3 | convert::TryInto as _, | |
4 | env, fmt, fs, | |
5 | path::{Path, PathBuf}, | |
6 | }; | |
7 | ||
8 | use chrono::{Datelike as _, TimeZone as _, Utc}; | |
9 | use glob::glob; | |
10 | use regex::Regex; | |
11 | ||
12 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] | |
13 | struct Date { | |
14 | year: u32, | |
15 | month: u32, | |
16 | } | |
17 | ||
18 | impl Date { | |
19 | fn months_since(self, other: Date) -> Option<u32> { | |
20 | let self_chrono = Utc.ymd(self.year.try_into().unwrap(), self.month, 1); | |
21 | let other_chrono = Utc.ymd(other.year.try_into().unwrap(), other.month, 1); | |
22 | let duration_since = self_chrono.signed_duration_since(other_chrono); | |
23 | let months_since = duration_since.num_days() / 30; | |
24 | if months_since < 0 { | |
25 | None | |
26 | } else { | |
27 | Some(months_since.try_into().unwrap()) | |
28 | } | |
29 | } | |
30 | } | |
31 | ||
32 | impl fmt::Display for Date { | |
33 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
34 | write!(f, "{:04}-{:02}", self.year, self.month) | |
35 | } | |
36 | } | |
37 | ||
38 | fn make_date_regex() -> Regex { | |
39 | Regex::new( | |
40 | r"(?x) # insignificant whitespace mode | |
41 | <!--\s* | |
94222f64 | 42 | [dD]ate:\s* |
6a06907d XL |
43 | (?P<y>\d{4}) # year |
44 | - | |
45 | (?P<m>\d{2}) # month | |
46 | \s*-->", | |
47 | ) | |
48 | .unwrap() | |
49 | } | |
50 | ||
51 | fn collect_dates_from_file(date_regex: &Regex, text: &str) -> Vec<(usize, Date)> { | |
52 | let mut line = 1; | |
53 | let mut end_of_last_cap = 0; | |
54 | date_regex | |
55 | .captures_iter(&text) | |
56 | .map(|cap| { | |
57 | ( | |
58 | cap.get(0).unwrap().range(), | |
59 | Date { | |
60 | year: cap["y"].parse().unwrap(), | |
61 | month: cap["m"].parse().unwrap(), | |
62 | }, | |
63 | ) | |
64 | }) | |
65 | .map(|(byte_range, date)| { | |
66 | line += text[end_of_last_cap..byte_range.end] | |
67 | .chars() | |
68 | .filter(|c| *c == '\n') | |
69 | .count(); | |
70 | end_of_last_cap = byte_range.end; | |
71 | (line, date) | |
72 | }) | |
73 | .collect() | |
74 | } | |
75 | ||
76 | fn collect_dates(paths: impl Iterator<Item = PathBuf>) -> BTreeMap<PathBuf, Vec<(usize, Date)>> { | |
77 | let date_regex = make_date_regex(); | |
78 | let mut data = BTreeMap::new(); | |
79 | for path in paths { | |
80 | let text = fs::read_to_string(&path).unwrap(); | |
81 | let dates = collect_dates_from_file(&date_regex, &text); | |
82 | if !dates.is_empty() { | |
83 | data.insert(path, dates); | |
84 | } | |
85 | } | |
86 | data | |
87 | } | |
88 | ||
89 | fn filter_dates( | |
90 | current_month: Date, | |
91 | min_months_since: u32, | |
92 | dates_by_file: impl Iterator<Item = (PathBuf, Vec<(usize, Date)>)>, | |
93 | ) -> impl Iterator<Item = (PathBuf, Vec<(usize, Date)>)> { | |
94 | dates_by_file | |
95 | .map(move |(path, dates)| { | |
96 | ( | |
97 | path, | |
98 | dates | |
99 | .into_iter() | |
100 | .filter(|(_, date)| { | |
101 | current_month | |
102 | .months_since(*date) | |
103 | .expect("found date that is after current month") | |
104 | >= min_months_since | |
105 | }) | |
106 | .collect::<Vec<_>>(), | |
107 | ) | |
108 | }) | |
109 | .filter(|(_, dates)| !dates.is_empty()) | |
110 | } | |
111 | ||
112 | fn main() { | |
113 | let root_dir = env::args() | |
114 | .nth(1) | |
115 | .expect("expect root Markdown directory as CLI argument"); | |
116 | let root_dir_path = Path::new(&root_dir); | |
117 | let glob_pat = format!("{}/**/*.md", root_dir); | |
118 | let today_chrono = Utc::today(); | |
119 | let current_month = Date { | |
120 | year: today_chrono.year_ce().1, | |
121 | month: today_chrono.month(), | |
122 | }; | |
123 | ||
124 | let dates_by_file = collect_dates(glob(&glob_pat).unwrap().map(Result::unwrap)); | |
125 | let dates_by_file: BTreeMap<_, _> = | |
126 | filter_dates(current_month, 6, dates_by_file.into_iter()).collect(); | |
127 | ||
128 | if dates_by_file.is_empty() { | |
129 | println!("empty"); | |
130 | } else { | |
131 | println!("Date Reference Triage for {}", current_month); | |
132 | println!("## Procedure"); | |
133 | println!(); | |
134 | println!( | |
135 | "Each of these dates should be checked to see if the docs they annotate are \ | |
136 | up-to-date. Each date should be updated (in the Markdown file where it appears) to \ | |
137 | use the current month ({current_month}), or removed if the docs it annotates are not \ | |
138 | expected to fall out of date quickly.", | |
139 | current_month = current_month | |
140 | ); | |
141 | println!(); | |
142 | println!( | |
143 | "Please check off each date once a PR to update it (and, if applicable, its \ | |
144 | surrounding docs) has been merged. Please also mention that you are working on a \ | |
145 | particular set of dates so duplicate work is avoided." | |
146 | ); | |
147 | println!(); | |
148 | println!("Finally, once all the dates have been updated, please close this issue."); | |
149 | println!(); | |
150 | println!("## Dates"); | |
151 | println!(); | |
152 | ||
153 | for (path, dates) in dates_by_file { | |
154 | println!( | |
155 | "- [ ] {}", | |
156 | path.strip_prefix(&root_dir_path).unwrap().display() | |
157 | ); | |
158 | for (line, date) in dates { | |
159 | println!(" - [ ] line {}: {}", line, date); | |
160 | } | |
161 | } | |
162 | println!(); | |
163 | } | |
164 | } | |
165 | ||
166 | #[cfg(test)] | |
167 | mod tests { | |
168 | use super::*; | |
169 | ||
170 | #[test] | |
171 | fn test_months_since() { | |
172 | let date1 = Date { | |
173 | year: 2020, | |
174 | month: 3, | |
175 | }; | |
176 | let date2 = Date { | |
177 | year: 2021, | |
178 | month: 1, | |
179 | }; | |
180 | assert_eq!(date2.months_since(date1), Some(10)); | |
181 | } | |
182 | ||
183 | #[test] | |
184 | fn test_date_regex() { | |
185 | let regex = make_date_regex(); | |
186 | assert!(regex.is_match("foo <!-- date: 2021-01 --> bar")); | |
187 | } | |
188 | ||
94222f64 XL |
189 | #[test] |
190 | fn test_date_regex_capitalized() { | |
191 | let regex = make_date_regex(); | |
192 | assert!(regex.is_match("foo <!-- Date: 2021-08 --> bar")); | |
193 | } | |
194 | ||
6a06907d XL |
195 | #[test] |
196 | fn test_collect_dates_from_file() { | |
197 | let text = "Test1\n<!-- date: 2021-01 -->\nTest2\nFoo<!-- date: 2021-02 \ | |
198 | -->\nTest3\nTest4\nFoo<!-- date: 2021-03 -->Bar\n<!-- date: 2021-04 \ | |
199 | -->\nTest5\nTest6\nTest7\n<!-- date: \n\n2021-05 -->\nTest8 | |
200 | "; | |
201 | assert_eq!( | |
202 | collect_dates_from_file(&make_date_regex(), text), | |
203 | vec![ | |
204 | ( | |
205 | 2, | |
206 | Date { | |
207 | year: 2021, | |
208 | month: 1, | |
209 | } | |
210 | ), | |
211 | ( | |
212 | 4, | |
213 | Date { | |
214 | year: 2021, | |
215 | month: 2, | |
216 | } | |
217 | ), | |
218 | ( | |
219 | 7, | |
220 | Date { | |
221 | year: 2021, | |
222 | month: 3, | |
223 | } | |
224 | ), | |
225 | ( | |
226 | 8, | |
227 | Date { | |
228 | year: 2021, | |
229 | month: 4, | |
230 | } | |
231 | ), | |
232 | ( | |
233 | 14, | |
234 | Date { | |
235 | year: 2021, | |
236 | month: 5, | |
237 | } | |
238 | ), | |
239 | ] | |
240 | ); | |
241 | } | |
242 | } |