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

157 lines
6.5 KiB
Rust

//! Tests of the `tp.date` module (spec D3).
use notescraft_bases::Value;
use notescraft_templater::{DateCtx, Dur, add_dur, call_date, parse_iso_duration};
use notescraft_templates::{Date, DateTime, Weekday};
/// The reference moment of the tests: Tuesday, 2026-10-13 15:04:05.
fn now() -> DateTime {
DateTime::new(Date::new(2026, 10, 13).unwrap(), 15, 4, 5).unwrap()
}
/// A context whose week starts on Monday.
fn monday_ctx() -> DateCtx {
DateCtx { now: now(), week_start: Weekday::Monday }
}
/// A context whose week starts on Sunday.
fn sunday_ctx() -> DateCtx {
DateCtx { now: now(), week_start: Weekday::Sunday }
}
/// A number argument.
fn number(value: f64) -> Value {
Value::Number(value)
}
/// A string argument.
fn text(value: &str) -> Value {
Value::Str(value.to_owned())
}
/// Calls the module and returns the formatted date.
fn call(name: &str, args: &[Value], ctx: &DateCtx) -> String {
match call_date(name, args, ctx) {
Ok(Value::Str(text)) => text,
Ok(other) => panic!("tp.date.{name} returned {other:?}"),
Err(error) => panic!("tp.date.{name} failed: {error}"),
}
}
#[test]
fn now_formats_the_current_date() {
let ctx = monday_ctx();
assert_eq!(call("now", &[text("YYYY-MM-DD")], &ctx), "2026-10-13");
assert_eq!(call("now", &[text("dddd")], &ctx), "Tuesday");
assert_eq!(call("now", &[], &ctx), "2026-10-13");
assert_eq!(call("now", &[text("YYYY-MM-DDTHH:mm:ss")], &ctx), "2026-10-13T15:04:05");
}
#[test]
fn now_takes_a_day_offset() {
let ctx = monday_ctx();
assert_eq!(call("now", &[text("YYYY-MM-DD"), number(7.0)], &ctx), "2026-10-20");
assert_eq!(call("now", &[text("YYYY-MM-DD"), number(-7.0)], &ctx), "2026-10-06");
assert_eq!(call("now", &[text("YYYY-MM-DD"), number(0.0)], &ctx), "2026-10-13");
}
#[test]
fn now_takes_an_iso_offset() {
let ctx = monday_ctx();
assert_eq!(call("now", &[text("YYYY-MM-DD"), text("P1Y")], &ctx), "2027-10-13");
assert_eq!(call("now", &[text("YYYY-MM-DD"), text("-P1W")], &ctx), "2026-10-06");
assert_eq!(call("now", &[text("YYYY-MM-DD"), text("P1M2D")], &ctx), "2026-11-15");
assert_eq!(call("now", &[text("YYYY-MM-DD HH:mm"), text("PT2H")], &ctx), "2026-10-13 17:04");
}
#[test]
fn tomorrow_and_yesterday_move_one_day() {
let ctx = monday_ctx();
assert_eq!(call("tomorrow", &[text("YYYY-MM-DD")], &ctx), "2026-10-14");
assert_eq!(call("yesterday", &[text("YYYY-MM-DD")], &ctx), "2026-10-12");
assert_eq!(call("tomorrow", &[text("YYYY-MM-DD"), number(2.0)], &ctx), "2026-10-16");
assert_eq!(call("yesterday", &[text("YYYY-MM-DD"), text("-P1W")], &ctx), "2026-10-05");
}
#[test]
fn weekday_counts_from_a_monday_week() {
let ctx = monday_ctx();
assert_eq!(call("weekday", &[text("YYYY-MM-DD"), number(0.0)], &ctx), "2026-10-12");
assert_eq!(call("weekday", &[text("YYYY-MM-DD"), number(1.0)], &ctx), "2026-10-13");
assert_eq!(call("weekday", &[text("YYYY-MM-DD"), number(7.0)], &ctx), "2026-10-19");
assert_eq!(call("weekday", &[text("YYYY-MM-DD"), number(-7.0)], &ctx), "2026-10-05");
assert_eq!(call("weekday", &[], &ctx), "Monday");
}
#[test]
fn weekday_counts_from_a_sunday_week() {
let ctx = sunday_ctx();
assert_eq!(call("weekday", &[text("YYYY-MM-DD"), number(0.0)], &ctx), "2026-10-11");
assert_eq!(call("weekday", &[text("YYYY-MM-DD"), number(1.0)], &ctx), "2026-10-12");
assert_eq!(call("weekday", &[text("YYYY-MM-DD"), number(7.0)], &ctx), "2026-10-18");
assert_eq!(call("weekday", &[text("YYYY-MM-DD"), number(-7.0)], &ctx), "2026-10-04");
}
#[test]
fn a_reference_replaces_the_current_date() {
let ctx = monday_ctx();
let args = [text("YYYY-MM-DD"), number(0.0), text("2020-05-01"), text("YYYY-MM-DD")];
assert_eq!(call("now", &args, &ctx), "2020-05-01");
let shifted = [text("YYYY-MM-DD"), number(1.0), text("2020-05-01"), text("YYYY-MM-DD")];
assert_eq!(call("now", &shifted, &ctx), "2020-05-02");
let default_format = [text("YYYY-MM-DD"), number(0.0), text("2020-05-01")];
assert_eq!(call("now", &default_format, &ctx), "2020-05-01");
}
#[test]
fn a_reference_reads_a_date_out_of_a_title() {
let ctx = monday_ctx();
let title = [text("YYYY-MM-DD"), number(0.0), text("Sprint 2020-05-01 review"), text("YYYY-MM-DD")];
assert_eq!(call("now", &title, &ctx), "2020-05-01");
let stamp = [text("YYYY-MM-DD HH:mm"), number(0.0), text("Release 2020-05-01 09:30"), text("YYYY-MM-DD HH:mm")];
assert_eq!(call("now", &stamp, &ctx), "2020-05-01 09:30");
}
#[test]
fn a_month_ends_on_the_last_day_of_its_month() {
let ctx = monday_ctx();
let january = [text("YYYY-MM-DD"), text("P1M"), text("2026-01-31"), text("YYYY-MM-DD")];
assert_eq!(call("now", &january, &ctx), "2026-02-28");
let leap = [text("YYYY-MM-DD"), text("P1M"), text("2024-01-31"), text("YYYY-MM-DD")];
assert_eq!(call("now", &leap, &ctx), "2024-02-29");
let start = DateTime::new(Date::new(2026, 1, 31).unwrap(), 12, 0, 0).unwrap();
let month = parse_iso_duration("P1M").unwrap();
let shifted = add_dur(start, month).unwrap();
assert_eq!(shifted.date(), Date::new(2026, 2, 28).unwrap());
}
#[test]
fn iso_durations_parse() {
assert_eq!(parse_iso_duration("P1Y"), Some(Dur { years: 1, months: 0, days: 0, secs: 0 }));
assert_eq!(parse_iso_duration("-P1W"), Some(Dur { years: 0, months: 0, days: -7, secs: 0 }));
assert_eq!(parse_iso_duration("P1M2D"), Some(Dur { years: 0, months: 1, days: 2, secs: 0 }));
assert_eq!(parse_iso_duration("PT2H"), Some(Dur { years: 0, months: 0, days: 0, secs: 7200 }));
assert_eq!(parse_iso_duration("P1Y2M3DT4H5M6S"), Some(Dur { years: 1, months: 2, days: 3, secs: 14_706 }));
for text in ["", "P", "PT", "1Y", "P1Q", "P1H"] {
assert_eq!(parse_iso_duration(text), None, "{text}");
}
}
#[test]
fn bad_arguments_are_refused() {
let ctx = monday_ctx();
assert!(call_date("now", &[text("YYYY-MM-DD"), text("P1Q")], &ctx).is_err());
assert!(call_date("now", &[Value::Bool(true)], &ctx).is_err());
assert!(call_date("now", &[text("YYYY-MM-DD"), number(0.0), text("not a date")], &ctx).is_err());
assert!(call_date("now", &[text("YYYY-MM-DD"), Value::Bool(true)], &ctx).is_err());
assert!(call_date("weekday", &[text("YYYY-MM-DD"), text("monday")], &ctx).is_err());
assert!(call_date("unknown", &[], &ctx).is_err());
}
#[test]
fn unknown_format_characters_pass_through() {
let ctx = monday_ctx();
assert_eq!(call("now", &[text("[YYYY] YYYY!")], &ctx), "YYYY 2026!");
assert_eq!(call("now", &[text("QQQ")], &ctx), "QQQ");
}