b8586537f8
c63ec67 dropped diary_start_week_day, hardwiring the weekly diary to ISO
(Monday) weeks. Upstream vimwiki instead names a weekly note by the
week-start day's date (YYYY-MM-DD) and honours diary_start_week_day. Rather
than force one scheme, make it a per-wiki choice so migrators keep upstream
behaviour while existing nuwiki weekly files keep working:
- New per-wiki key `diary_weekly_style`: `iso` (default — `YYYY-Www`,
Monday-based, nuwiki's original) or `date`/`vimwiki` (`YYYY-MM-DD` of the
week-start day, upstream parity).
- Restored per-wiki key `diary_start_week_day` (`monday`..`sunday`, default
monday); applies only in `date` mode.
Implementation:
- nuwiki-core::date gains WeeklyStyle, WeekStart, and DiaryCalendar (owns
today/next/prev — date-mode snaps to the week-start and steps ±7 days;
iso/daily/monthly/yearly defer to the existing DiaryPeriod logic).
- WikiConfig gains the two fields (+ defaults, RawWiki, From) and a
diary_calendar() builder; commands.rs diary_open_relative and the
next/prev pivot use it.
Defaults preserve current behaviour (iso/monday), so no breaking change.
Tests: DiaryCalendar cases in nuwiki-core/tests/diary.rs (snap, ±7, sunday
start, iso delegation, daily) + config round-trip in
nuwiki-lsp/tests/index_and_config.rs. README + doc/nuwiki.txt + lua config
comment updated. fmt + clippy clean; all crate tests + config-parity green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
279 lines
7.6 KiB
Rust
279 lines
7.6 KiB
Rust
//! Cluster 4 — diary frequency / period primitives.
|
|
//! Tests the date math for ISO weeks, monthly, yearly periods.
|
|
|
|
use nuwiki_core::date::{
|
|
DiaryCalendar, DiaryDate, DiaryFrequency, DiaryPeriod, WeekStart, WeeklyStyle,
|
|
};
|
|
|
|
#[test]
|
|
fn frequency_parses_canonical_strings() {
|
|
assert_eq!(DiaryFrequency::parse("daily"), DiaryFrequency::Daily);
|
|
assert_eq!(DiaryFrequency::parse("weekly"), DiaryFrequency::Weekly);
|
|
assert_eq!(DiaryFrequency::parse("monthly"), DiaryFrequency::Monthly);
|
|
assert_eq!(DiaryFrequency::parse("yearly"), DiaryFrequency::Yearly);
|
|
assert_eq!(DiaryFrequency::parse("WeEkLy"), DiaryFrequency::Weekly);
|
|
// Unknown falls back to Daily (mirrors vimwiki's permissive behaviour).
|
|
assert_eq!(DiaryFrequency::parse("hourly"), DiaryFrequency::Daily);
|
|
assert_eq!(DiaryFrequency::parse(""), DiaryFrequency::Daily);
|
|
}
|
|
|
|
#[test]
|
|
fn period_format_round_trips() {
|
|
let cases = [
|
|
(
|
|
"2026-05-12",
|
|
DiaryPeriod::Day(DiaryDate::from_ymd(2026, 5, 12).unwrap()),
|
|
),
|
|
(
|
|
"2026-W19",
|
|
DiaryPeriod::Week {
|
|
iso_year: 2026,
|
|
iso_week: 19,
|
|
},
|
|
),
|
|
(
|
|
"2026-05",
|
|
DiaryPeriod::Month {
|
|
year: 2026,
|
|
month: 5,
|
|
},
|
|
),
|
|
("2026", DiaryPeriod::Year { year: 2026 }),
|
|
];
|
|
for (s, want) in cases {
|
|
let parsed = DiaryPeriod::parse(s).unwrap_or_else(|| panic!("parse {s}"));
|
|
assert_eq!(parsed, want, "parse({s})");
|
|
assert_eq!(parsed.format(), s, "format({s})");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn period_parse_rejects_garbage() {
|
|
assert!(DiaryPeriod::parse("garbage").is_none());
|
|
assert!(DiaryPeriod::parse("2026-W00").is_none());
|
|
assert!(DiaryPeriod::parse("2026-13").is_none()); // month 13
|
|
assert!(DiaryPeriod::parse("2026-").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn iso_week_jan_1_can_belong_to_prior_year() {
|
|
// 2021-01-01 was a Friday → ISO week 53 of 2020.
|
|
let d = DiaryDate::from_ymd(2021, 1, 1).unwrap();
|
|
let p = DiaryPeriod::week_containing(d);
|
|
assert_eq!(
|
|
p,
|
|
DiaryPeriod::Week {
|
|
iso_year: 2020,
|
|
iso_week: 53
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn iso_week_dec_31_can_belong_to_next_year() {
|
|
// 2024-12-30 (Monday) is in ISO week 1 of 2025.
|
|
let d = DiaryDate::from_ymd(2024, 12, 30).unwrap();
|
|
let p = DiaryPeriod::week_containing(d);
|
|
assert_eq!(
|
|
p,
|
|
DiaryPeriod::Week {
|
|
iso_year: 2025,
|
|
iso_week: 1
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn iso_week_mid_year_is_consistent() {
|
|
// 2026-05-12 is a Tuesday in ISO week 20.
|
|
let d = DiaryDate::from_ymd(2026, 5, 12).unwrap();
|
|
let p = DiaryPeriod::week_containing(d);
|
|
assert_eq!(
|
|
p,
|
|
DiaryPeriod::Week {
|
|
iso_year: 2026,
|
|
iso_week: 20
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn week_next_and_prev_step_by_seven_days() {
|
|
let p = DiaryPeriod::Week {
|
|
iso_year: 2026,
|
|
iso_week: 20,
|
|
};
|
|
assert_eq!(
|
|
p.next(),
|
|
DiaryPeriod::Week {
|
|
iso_year: 2026,
|
|
iso_week: 21
|
|
}
|
|
);
|
|
assert_eq!(
|
|
p.prev(),
|
|
DiaryPeriod::Week {
|
|
iso_year: 2026,
|
|
iso_week: 19
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn week_next_crosses_year_boundary() {
|
|
// Week 53 of 2020 → week 53 ends; next should be week 1 of 2021.
|
|
let p = DiaryPeriod::Week {
|
|
iso_year: 2020,
|
|
iso_week: 53,
|
|
};
|
|
assert_eq!(
|
|
p.next(),
|
|
DiaryPeriod::Week {
|
|
iso_year: 2021,
|
|
iso_week: 1
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn month_next_wraps_to_january() {
|
|
let dec = DiaryPeriod::Month {
|
|
year: 2026,
|
|
month: 12,
|
|
};
|
|
assert_eq!(
|
|
dec.next(),
|
|
DiaryPeriod::Month {
|
|
year: 2027,
|
|
month: 1
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn month_prev_wraps_to_december() {
|
|
let jan = DiaryPeriod::Month {
|
|
year: 2026,
|
|
month: 1,
|
|
};
|
|
assert_eq!(
|
|
jan.prev(),
|
|
DiaryPeriod::Month {
|
|
year: 2025,
|
|
month: 12
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn year_next_and_prev_increment_by_one() {
|
|
let y = DiaryPeriod::Year { year: 2026 };
|
|
assert_eq!(y.next(), DiaryPeriod::Year { year: 2027 });
|
|
assert_eq!(y.prev(), DiaryPeriod::Year { year: 2025 });
|
|
}
|
|
|
|
#[test]
|
|
fn day_next_and_prev_still_work() {
|
|
let d = DiaryPeriod::Day(DiaryDate::from_ymd(2026, 5, 12).unwrap());
|
|
assert_eq!(
|
|
d.next(),
|
|
DiaryPeriod::Day(DiaryDate::from_ymd(2026, 5, 13).unwrap())
|
|
);
|
|
assert_eq!(
|
|
d.prev(),
|
|
DiaryPeriod::Day(DiaryDate::from_ymd(2026, 5, 11).unwrap())
|
|
);
|
|
}
|
|
|
|
// ===== DiaryCalendar — weekly naming styles + week-start =====
|
|
// 2026-05-25 is a Monday (so 2026-05-27 is a Wednesday, 2026-05-24 a Sunday).
|
|
|
|
fn day(y: i32, m: u8, d: u8) -> DiaryPeriod {
|
|
DiaryPeriod::Day(DiaryDate::from_ymd(y, m, d).unwrap())
|
|
}
|
|
|
|
#[test]
|
|
fn weekly_style_and_week_start_parse() {
|
|
assert_eq!(WeeklyStyle::parse("date"), WeeklyStyle::Date);
|
|
assert_eq!(WeeklyStyle::parse("vimwiki"), WeeklyStyle::Date);
|
|
assert_eq!(WeeklyStyle::parse("iso"), WeeklyStyle::Iso);
|
|
assert_eq!(WeeklyStyle::parse(""), WeeklyStyle::Iso); // default
|
|
assert_eq!(WeeklyStyle::parse("WEEK"), WeeklyStyle::Iso);
|
|
// WeekStart::parse is exercised via the calendar tests below; unknown
|
|
// names fall back to Monday.
|
|
assert_eq!(WeekStart::parse("sunday"), WeekStart::parse("SUNDAY"));
|
|
assert_eq!(WeekStart::parse("nonsense"), WeekStart::monday());
|
|
}
|
|
|
|
#[test]
|
|
fn date_mode_weekly_today_is_a_day() {
|
|
let cal = DiaryCalendar::new(
|
|
DiaryFrequency::Weekly,
|
|
WeeklyStyle::Date,
|
|
WeekStart::monday(),
|
|
);
|
|
assert!(matches!(cal.today(), DiaryPeriod::Day(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn iso_mode_weekly_today_is_a_week() {
|
|
let cal = DiaryCalendar::new(
|
|
DiaryFrequency::Weekly,
|
|
WeeklyStyle::Iso,
|
|
WeekStart::monday(),
|
|
);
|
|
assert!(matches!(cal.today(), DiaryPeriod::Week { .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn date_mode_weekly_steps_seven_days_from_the_week_start_monday() {
|
|
let cal = DiaryCalendar::new(
|
|
DiaryFrequency::Weekly,
|
|
WeeklyStyle::Date,
|
|
WeekStart::monday(),
|
|
);
|
|
// From a Wednesday: snap back to Mon 05-25, then ±7.
|
|
assert_eq!(cal.next(day(2026, 5, 27)), day(2026, 6, 1));
|
|
assert_eq!(cal.prev(day(2026, 5, 27)), day(2026, 5, 18));
|
|
// From the Monday itself: no snap, just ±7.
|
|
assert_eq!(cal.next(day(2026, 5, 25)), day(2026, 6, 1));
|
|
assert_eq!(cal.prev(day(2026, 5, 25)), day(2026, 5, 18));
|
|
// Stem is the plain date, like upstream.
|
|
assert_eq!(cal.next(day(2026, 5, 27)).format(), "2026-06-01");
|
|
}
|
|
|
|
#[test]
|
|
fn date_mode_weekly_honours_a_sunday_week_start() {
|
|
let cal = DiaryCalendar::new(
|
|
DiaryFrequency::Weekly,
|
|
WeeklyStyle::Date,
|
|
WeekStart::parse("sunday"),
|
|
);
|
|
// Wed 05-27 snaps back to Sun 05-24, +7 = Sun 05-31.
|
|
assert_eq!(cal.next(day(2026, 5, 27)), day(2026, 5, 31));
|
|
assert_eq!(cal.prev(day(2026, 5, 27)), day(2026, 5, 17));
|
|
}
|
|
|
|
#[test]
|
|
fn iso_mode_weekly_delegates_to_iso_week_stepping() {
|
|
let cal = DiaryCalendar::new(
|
|
DiaryFrequency::Weekly,
|
|
WeeklyStyle::Iso,
|
|
WeekStart::monday(),
|
|
);
|
|
let wk = DiaryPeriod::week_containing(DiaryDate::from_ymd(2026, 5, 27).unwrap());
|
|
assert_eq!(cal.next(wk), wk.next());
|
|
assert_eq!(cal.prev(wk), wk.prev());
|
|
}
|
|
|
|
#[test]
|
|
fn daily_calendar_steps_one_day_regardless_of_weekly_fields() {
|
|
let cal = DiaryCalendar::new(
|
|
DiaryFrequency::Daily,
|
|
WeeklyStyle::Date,
|
|
WeekStart::parse("sunday"),
|
|
);
|
|
assert_eq!(cal.next(day(2026, 5, 27)), day(2026, 5, 28));
|
|
assert_eq!(cal.prev(day(2026, 5, 27)), day(2026, 5, 26));
|
|
}
|