138 lines
4.7 KiB
Rust
138 lines
4.7 KiB
Rust
//! Recurrence rules: the grammar of the `Tasks` plugin and the next date.
|
|
|
|
use notescraft_tasks::Rule;
|
|
use notescraft_templates::Date;
|
|
|
|
/// A date of the tests.
|
|
fn date(year: i32, month: u8, day: u8) -> Date {
|
|
Date::new(year, month, day).expect("a valid date")
|
|
}
|
|
|
|
/// A date of the tests, as a year, a month and a day.
|
|
type Day = (i32, u8, u8);
|
|
|
|
/// The rules of the grammar: the text, the date it starts from and the date
|
|
/// that follows.
|
|
const CASES: [(&str, Day, Day); 19] = [
|
|
("every day", (2026, 10, 23), (2026, 10, 24)),
|
|
("every 3 days", (2026, 10, 23), (2026, 10, 26)),
|
|
("every weekday", (2026, 10, 23), (2026, 10, 26)),
|
|
("every week", (2026, 10, 23), (2026, 10, 30)),
|
|
("every 2 weeks", (2026, 10, 23), (2026, 11, 6)),
|
|
("every week on Tuesday", (2026, 10, 21), (2026, 10, 27)),
|
|
("every week on Tuesday, Friday", (2026, 10, 21), (2026, 10, 23)),
|
|
("every month", (2026, 1, 31), (2026, 2, 28)),
|
|
("every 3 months", (2026, 1, 31), (2026, 4, 30)),
|
|
("every month on the 1st", (2026, 1, 31), (2026, 2, 1)),
|
|
("every month on the last", (2026, 1, 31), (2026, 2, 28)),
|
|
("every month on the last Friday", (2026, 10, 23), (2026, 10, 30)),
|
|
("every month on the 2nd Wednesday", (2026, 1, 31), (2026, 2, 11)),
|
|
("every month on the 31st", (2026, 1, 31), (2026, 3, 31)),
|
|
("every year", (2026, 10, 23), (2027, 10, 23)),
|
|
("every 2 years", (2026, 10, 23), (2028, 10, 23)),
|
|
("every January on the 15th", (2026, 1, 1), (2026, 1, 15)),
|
|
("every April and December on the 1st and 24th", (2026, 1, 15), (2026, 4, 1)),
|
|
("every day when done", (2026, 10, 23), (2026, 10, 24)),
|
|
];
|
|
|
|
#[test]
|
|
fn every_rule_of_the_grammar_has_a_next_date() {
|
|
for (text, from, next) in CASES {
|
|
let rule = Rule::parse(text).expect("a rule");
|
|
let from = date(from.0, from.1, from.2);
|
|
let next = date(next.0, next.1, next.2);
|
|
assert_eq!(rule.next(from), Some(next), "{text}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_monthly_rule_keeps_going_from_the_clamped_date() {
|
|
let rule = Rule::parse("every month").expect("a rule");
|
|
let first = rule.next(date(2026, 1, 31)).expect("a next date");
|
|
assert_eq!(first, date(2026, 2, 28));
|
|
assert_eq!(rule.next(first), Some(date(2026, 3, 28)));
|
|
}
|
|
|
|
#[test]
|
|
fn a_monthly_rule_on_the_31st_skips_november() {
|
|
let rule = Rule::parse("every month on the 31st").expect("a rule");
|
|
assert_eq!(rule.next(date(2026, 10, 31)), Some(date(2026, 12, 31)));
|
|
}
|
|
|
|
#[test]
|
|
fn a_yearly_rule_falls_on_every_day_it_names() {
|
|
let rule = Rule::parse("every April and December on the 1st and 24th").expect("a rule");
|
|
assert_eq!(rule.next(date(2027, 4, 1)), Some(date(2027, 4, 24)));
|
|
assert_eq!(rule.next(date(2027, 12, 24)), Some(date(2028, 4, 1)));
|
|
}
|
|
|
|
#[test]
|
|
fn the_canonical_text_is_the_text_of_the_plugin() {
|
|
let texts = [
|
|
"every day",
|
|
"every 3 days",
|
|
"every weekday",
|
|
"every week",
|
|
"every 2 weeks",
|
|
"every week on Tuesday, Friday",
|
|
"every month",
|
|
"every 3 months",
|
|
"every month on the 1st and 24th",
|
|
"every month on the last",
|
|
"every month on the last Friday",
|
|
"every month on the 2nd Wednesday",
|
|
"every year",
|
|
"every 2 years",
|
|
"every January on the 15th",
|
|
"every April and December on the 1st and 24th",
|
|
"every day when done",
|
|
"every week when done",
|
|
];
|
|
for text in texts {
|
|
let rule = Rule::parse(text).expect("a rule");
|
|
assert_eq!(rule.to_text(), text);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_canonical_text_sorts_the_days_and_the_months() {
|
|
let rule = Rule::parse("every week on friday, tuesday").expect("a rule");
|
|
assert_eq!(rule.to_text(), "every week on Tuesday, Friday");
|
|
let rule = Rule::parse("every december and april on the 24th, 1st").expect("a rule");
|
|
assert_eq!(rule.to_text(), "every April and December on the 1st and 24th");
|
|
}
|
|
|
|
#[test]
|
|
fn only_the_rules_that_end_in_when_done_use_the_completion_date() {
|
|
assert!(Rule::parse("every week when done").expect("a rule").when_done());
|
|
assert!(!Rule::parse("every week").expect("a rule").when_done());
|
|
}
|
|
|
|
#[test]
|
|
fn a_rule_that_is_not_in_the_grammar_is_refused() {
|
|
let texts = [
|
|
"",
|
|
"every",
|
|
"daily",
|
|
"FREQ=WEEKLY",
|
|
"every 0 days",
|
|
"every 1001 days",
|
|
"every day on Monday",
|
|
"every month on the 32nd",
|
|
"every month on Monday",
|
|
"every week on Funday",
|
|
"every week on the 1st",
|
|
"every April on Funday",
|
|
"every day when done extra",
|
|
];
|
|
for text in texts {
|
|
assert!(Rule::parse(text).is_err(), "{text} should be refused");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_rule_that_is_too_long_is_refused() {
|
|
let text = format!("every day {}", "x".repeat(120));
|
|
assert!(Rule::parse(&text).is_err());
|
|
}
|