//! 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)); }