262 lines
8.2 KiB
Rust
262 lines
8.2 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::convert::TryInto as _;
|
|
use std::path::{Path, PathBuf};
|
|
use std::str::FromStr;
|
|
use std::{env, fmt, fs, process};
|
|
|
|
use chrono::{Datelike as _, Month, TimeZone as _, Utc};
|
|
use glob::glob;
|
|
use regex::Regex;
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
struct Date {
|
|
year: u32,
|
|
month: u32,
|
|
}
|
|
|
|
impl Date {
|
|
fn months_since(self, other: Date) -> Option<u32> {
|
|
let self_chrono =
|
|
Utc.with_ymd_and_hms(self.year.try_into().unwrap(), self.month, 1, 0, 0, 0).unwrap();
|
|
let other_chrono =
|
|
Utc.with_ymd_and_hms(other.year.try_into().unwrap(), other.month, 1, 0, 0, 0).unwrap();
|
|
let duration_since = self_chrono.signed_duration_since(other_chrono);
|
|
let months_since = duration_since.num_days() / 30;
|
|
if months_since < 0 { None } else { Some(months_since.try_into().unwrap()) }
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Date {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
write!(f, "{:04}-{:02}", self.year, self.month)
|
|
}
|
|
}
|
|
|
|
fn make_date_regex() -> Regex {
|
|
Regex::new(
|
|
r"(?x) # insignificant whitespace mode
|
|
(<!--\s*
|
|
date-check:\s*
|
|
(?P<m1>[[:alpha:]]+)\s+
|
|
(?P<y1>\d{4})\s*-->
|
|
)
|
|
|
|
|
(<!--\s*
|
|
date-check\s*-->\s+
|
|
(?P<m2>[[:alpha:]]+)\s+
|
|
(?P<y2>\d{4})\b
|
|
)
|
|
",
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn collect_dates_from_file(date_regex: &Regex, text: &str) -> Vec<(usize, Date)> {
|
|
let mut line = 1;
|
|
let mut end_of_last_cap = 0;
|
|
date_regex
|
|
.captures_iter(text)
|
|
.filter_map(|cap| {
|
|
if let (Some(month), Some(year), None, None) | (None, None, Some(month), Some(year)) =
|
|
(cap.name("m1"), cap.name("y1"), cap.name("m2"), cap.name("y2"))
|
|
{
|
|
let year = year.as_str().parse().expect("year");
|
|
let month = Month::from_str(month.as_str()).expect("month").number_from_month();
|
|
Some((cap.get(0).expect("all").range(), Date { year, month }))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.map(|(byte_range, date)| {
|
|
line += text[end_of_last_cap..byte_range.end].chars().filter(|c| *c == '\n').count();
|
|
end_of_last_cap = byte_range.end;
|
|
(line, date)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn collect_dates(paths: impl Iterator<Item = PathBuf>) -> BTreeMap<PathBuf, Vec<(usize, Date)>> {
|
|
let date_regex = make_date_regex();
|
|
let mut data = BTreeMap::new();
|
|
for path in paths {
|
|
let text = fs::read_to_string(&path).unwrap();
|
|
let dates = collect_dates_from_file(&date_regex, &text);
|
|
if !dates.is_empty() {
|
|
data.insert(path, dates);
|
|
}
|
|
}
|
|
data
|
|
}
|
|
|
|
fn filter_dates(
|
|
current_month: Date,
|
|
min_months_since: u32,
|
|
dates_by_file: impl Iterator<Item = (PathBuf, Vec<(usize, Date)>)>,
|
|
) -> impl Iterator<Item = (PathBuf, Vec<(usize, Date)>)> {
|
|
dates_by_file
|
|
.map(move |(path, dates)| {
|
|
(
|
|
path,
|
|
dates
|
|
.into_iter()
|
|
.filter(|(_, date)| {
|
|
current_month
|
|
.months_since(*date)
|
|
.expect("found date that is after current month")
|
|
>= min_months_since
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
})
|
|
.filter(|(_, dates)| !dates.is_empty())
|
|
}
|
|
|
|
fn main() {
|
|
let mut args = env::args();
|
|
if args.len() == 1 {
|
|
eprintln!("error: expected root Markdown directory as CLI argument");
|
|
process::exit(1);
|
|
}
|
|
let root_dir = args.nth(1).unwrap();
|
|
let root_dir_path = Path::new(&root_dir);
|
|
let glob_pat = format!("{}/**/*.md", root_dir);
|
|
let today_chrono = Utc::now().date_naive();
|
|
let current_month = Date { year: today_chrono.year_ce().1, month: today_chrono.month() };
|
|
|
|
let dates_by_file = collect_dates(glob(&glob_pat).unwrap().map(Result::unwrap));
|
|
let dates_by_file: BTreeMap<_, _> =
|
|
filter_dates(current_month, 6, dates_by_file.into_iter()).collect();
|
|
|
|
if dates_by_file.is_empty() {
|
|
println!("empty");
|
|
} else {
|
|
println!("Date Reference Triage for {}", current_month);
|
|
println!("## Procedure");
|
|
println!();
|
|
println!(
|
|
"Each of these dates should be checked to see if the docs they annotate are \
|
|
up-to-date. Each date should be updated (in the Markdown file where it appears) to \
|
|
use the current month ({current_month}), or removed if the docs it annotates are not \
|
|
expected to fall out of date quickly.",
|
|
current_month = today_chrono.format("%B %Y"),
|
|
);
|
|
println!();
|
|
println!(
|
|
"Please check off each date once a PR to update it (and, if applicable, its \
|
|
surrounding docs) has been merged. Please also mention that you are working on a \
|
|
particular set of dates so duplicate work is avoided."
|
|
);
|
|
println!();
|
|
println!("Finally, once all the dates have been updated, please close this issue.");
|
|
println!();
|
|
println!("## Dates");
|
|
println!();
|
|
|
|
for (path, dates) in dates_by_file {
|
|
println!("- {}", path.strip_prefix(&root_dir_path).unwrap_or(&path).display(),);
|
|
for (line, date) in dates {
|
|
println!(" - [ ] line {}: {}", line, date);
|
|
}
|
|
}
|
|
println!();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_months_since() {
|
|
let date1 = Date { year: 2020, month: 3 };
|
|
let date2 = Date { year: 2021, month: 1 };
|
|
assert_eq!(date2.months_since(date1), Some(10));
|
|
}
|
|
|
|
#[test]
|
|
fn test_date_regex() {
|
|
let regex = &make_date_regex();
|
|
assert!(regex.is_match("<!-- date-check: jan 2021 -->"));
|
|
assert!(regex.is_match("<!-- date-check: january 2021 -->"));
|
|
assert!(regex.is_match("<!-- date-check: Jan 2021 -->"));
|
|
assert!(regex.is_match("<!-- date-check: January 2021 -->"));
|
|
assert!(regex.is_match("<!-- date-check --> jan 2021"));
|
|
assert!(regex.is_match("<!-- date-check --> january 2021"));
|
|
assert!(regex.is_match("<!-- date-check --> Jan 2021"));
|
|
assert!(regex.is_match("<!-- date-check --> January 2021"));
|
|
|
|
assert!(regex.is_match("<!-- date-check --> jan 2021 "));
|
|
assert!(regex.is_match("<!-- date-check --> jan 2021."));
|
|
}
|
|
|
|
#[test]
|
|
fn test_date_regex_fail() {
|
|
let regexes = &make_date_regex();
|
|
assert!(!regexes.is_match("<!-- date-check: jan 221 -->"));
|
|
assert!(!regexes.is_match("<!-- date-check: jan 20221 -->"));
|
|
assert!(!regexes.is_match("<!-- date-check: 01 2021 -->"));
|
|
assert!(!regexes.is_match("<!-- date-check --> jan 221"));
|
|
assert!(!regexes.is_match("<!-- date-check --> jan 20222"));
|
|
assert!(!regexes.is_match("<!-- date-check --> 01 2021"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_dates_from_file() {
|
|
let text = r"
|
|
Test1
|
|
<!-- date-check: jan 2021 -->
|
|
Test2
|
|
Foo<!-- date-check: february 2021
|
|
-->
|
|
Test3
|
|
Test4
|
|
Foo<!-- date-check: Mar 2021 -->Bar
|
|
<!-- date-check:April 2021
|
|
-->
|
|
Test5
|
|
Test6
|
|
Test7
|
|
<!-- date-check:
|
|
|
|
may 2021 -->
|
|
Test8
|
|
Test1
|
|
<!-- date-check --> jan 2021
|
|
Test2
|
|
Foo<!-- date-check
|
|
--> february 2021
|
|
Test3
|
|
Test4
|
|
Foo<!-- date-check --> mar 2021 Bar
|
|
<!-- date-check
|
|
--> apr 2021
|
|
Test5
|
|
Test6
|
|
Test7
|
|
<!-- date-check
|
|
|
|
--> may 2021
|
|
Test8
|
|
<!--
|
|
date-check
|
|
--> june 2021.
|
|
";
|
|
assert_eq!(
|
|
collect_dates_from_file(&make_date_regex(), text),
|
|
vec![
|
|
(3, Date { year: 2021, month: 1 }),
|
|
(6, Date { year: 2021, month: 2 }),
|
|
(9, Date { year: 2021, month: 3 }),
|
|
(11, Date { year: 2021, month: 4 }),
|
|
(17, Date { year: 2021, month: 5 }),
|
|
(20, Date { year: 2021, month: 1 }),
|
|
(23, Date { year: 2021, month: 2 }),
|
|
(26, Date { year: 2021, month: 3 }),
|
|
(28, Date { year: 2021, month: 4 }),
|
|
(34, Date { year: 2021, month: 5 }),
|
|
(38, Date { year: 2021, month: 6 }),
|
|
],
|
|
);
|
|
}
|
|
}
|