Files
Marcos Paulo 179a5cdc47
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: task recurrence rules, tp.date and durations, Lucide icon catalogue and decor snapshot (MiniMax M3.1 Flash)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 10:23:34 -03:00

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());
}