#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::items_after_statements,
clippy::many_single_char_names,
clippy::float_cmp
)]
use jiff::civil::date;
use timeglyph::cal::{build_day, CalDay};
use timeglyph::RenderZone;
fn day(y: i16, m: i8, d: i8) -> CalDay {
build_day(date(y, m, d), &RenderZone::Utc).unwrap()
}
fn zoned(y: i16, m: i8, d: i8, tz: &str) -> CalDay {
let zone = RenderZone::parse(tz).unwrap();
build_day(date(y, m, d), &zone).unwrap()
}
#[test]
fn jdn_mjd_unix_anchors() {
let a = day(2000, 1, 1);
assert_eq!(a.jdn, 2_451_545);
assert_eq!(a.mjd, 51_544);
assert_eq!(a.unix_utc_midnight, 946_684_800);
let e = day(1858, 11, 17);
assert_eq!(e.jdn, 2_400_001);
assert_eq!(e.mjd, 0);
let u = day(1970, 1, 1);
assert_eq!(u.jdn, 2_440_588);
assert_eq!(u.unix_utc_midnight, 0);
let c = day(2016, 12, 31);
assert_eq!(c.jdn, 2_457_754);
assert_eq!(c.mjd, 57_753);
assert_eq!(c.unix_utc_midnight, 1_483_142_400);
}
#[test]
fn iso_week_edges() {
let a = day(2000, 1, 1); assert_eq!((a.iso_year, a.iso_week, a.iso_weekday), (1999, 52, 6));
let b = day(2008, 12, 29); assert_eq!((b.iso_year, b.iso_week, b.iso_weekday), (2009, 1, 1));
let c = day(2010, 1, 3); assert_eq!((c.iso_year, c.iso_week, c.iso_weekday), (2009, 53, 7));
let d = day(2016, 12, 31); assert_eq!((d.iso_year, d.iso_week, d.iso_weekday), (2016, 52, 6));
let n = day(2026, 11, 1); assert_eq!((n.iso_year, n.iso_week, n.iso_weekday), (2026, 44, 7));
}
#[test]
fn day_of_year_weekday_and_iso_string() {
let c = day(2016, 12, 31);
assert_eq!((c.day_of_year, c.days_in_year), (366, 366)); let n = day(2026, 11, 1);
assert_eq!((n.day_of_year, n.days_in_year), (305, 365));
assert_eq!(day(2000, 1, 1).weekday, "saturday");
assert_eq!(day(1970, 1, 1).weekday, "thursday");
assert_eq!(n.weekday, "sunday");
assert_eq!(n.date, "2026-11-01");
}
#[cfg(feature = "leap")]
mod leapday {
use timeglyph::leap::{gps_week, leap_seconds_on_utc_day};
#[test]
fn leap_second_days_from_iers_table() {
assert_eq!(leap_seconds_on_utc_day(1_483_142_400), 1);
assert_eq!(leap_seconds_on_utc_day(1_435_622_400), 1);
assert_eq!(leap_seconds_on_utc_day(1_483_056_000), 0); assert_eq!(leap_seconds_on_utc_day(1_483_228_800), 0); }
#[test]
fn gps_week_anchors() {
assert_eq!(gps_week(315_964_800), 0); assert_eq!(gps_week(1_554_595_200), 2048); assert_eq!(gps_week(1_793_491_200), 2443); }
}
#[test]
fn dst_fold_day_new_york() {
let d = zoned(2026, 11, 1, "America/New_York");
assert_eq!(d.offset_start_seconds, -14400);
assert_eq!(d.offset_end_seconds, -18000);
assert_eq!(d.wall_day_seconds, 90_000);
let t = d.dst_transition.expect("a transition on the fold day");
assert_eq!(t.kind, "fold");
assert_eq!(t.at_utc, "2026-11-01T06:00:00Z");
}
#[test]
fn dst_gap_day_new_york() {
let d = zoned(2026, 3, 8, "America/New_York");
assert_eq!(d.offset_start_seconds, -18000);
assert_eq!(d.offset_end_seconds, -14400);
assert_eq!(d.wall_day_seconds, 82_800);
assert_eq!(d.dst_transition.expect("gap").kind, "gap");
}
#[test]
fn dst_thirty_minute_fold_lord_howe() {
let d = zoned(2026, 4, 5, "Australia/Lord_Howe");
assert_eq!(d.wall_day_seconds, 88_200);
assert_eq!(d.dst_transition.expect("fold").kind, "fold");
}
#[test]
fn ordinary_day_has_no_transition_and_full_wall_day() {
let d = zoned(2026, 7, 1, "America/New_York");
assert_eq!(d.wall_day_seconds, 86_400);
assert!(d.dst_transition.is_none());
assert_eq!(d.offset_start_seconds, -14400); assert_eq!(day(2026, 7, 1).wall_day_seconds, 86_400);
}
#[cfg(feature = "leap")]
#[test]
fn leap_and_gps_fold_into_calday() {
let c = day(2016, 12, 31);
assert_eq!(c.leap_second, 1);
assert_eq!(c.utc_day_seconds, 86_401);
assert!(c.in_leap_smear_window);
assert_eq!(c.gps_week, 1929);
let n = day(2026, 7, 1);
assert_eq!(n.leap_second, 0);
assert_eq!(n.utc_day_seconds, 86_400);
assert!(!n.in_leap_smear_window);
}
fn has_artifact(d: &CalDay, kind: &str, name: &str) -> bool {
d.artifacts.iter().any(|a| a.kind == kind && a.name == name)
}
#[test]
fn epoch_days_come_from_the_registry() {
assert!(has_artifact(&day(1601, 1, 1), "epoch", "filetime"));
assert!(has_artifact(&day(1601, 1, 1), "epoch", "webkit"));
assert!(has_artifact(&day(1970, 1, 1), "epoch", "unix"));
assert!(has_artifact(&day(1899, 12, 30), "epoch", "ole"));
assert!(has_artifact(&day(1904, 1, 1), "epoch", "hfsplus"));
assert!(has_artifact(&day(2001, 1, 1), "epoch", "cocoa"));
}
#[test]
fn rollovers_are_derived_from_structural_limits() {
assert!(has_artifact(&day(2038, 1, 19), "rollover", "unix_i32"));
assert!(has_artifact(&day(2106, 2, 7), "rollover", "unix_u32"));
}
#[test]
fn ordinary_day_has_no_artifacts() {
assert!(day(2026, 7, 1).artifacts.is_empty());
}
#[cfg(feature = "lunisolar")]
#[test]
fn chinese_overlay_matches_lunisolar() {
let d = zoned(2020, 5, 31, "+08:00");
let c = d.alt_chinese.expect("chinese overlay under lunisolar");
assert_eq!(c.year_pillar, "庚子");
assert_eq!(c.lunar_month, 4);
use timeglyph::{lunisolar, PosixNs, RenderZone};
let zone = RenderZone::parse("+08:00").unwrap();
let noon = jiff::civil::date(2020, 5, 31)
.at(12, 0, 0, 0)
.to_zoned(jiff::tz::TimeZone::fixed(jiff::tz::offset(8)))
.unwrap()
.timestamp()
.as_nanosecond();
let r = lunisolar::render(PosixNs(noon), &zone, None).unwrap();
assert_eq!(c.lunar_year, r.lunar_year);
assert_eq!(c.lunar_day, r.lunar_day);
assert_eq!(c.solar_term, r.solar_term);
}
#[cfg(feature = "altcal")]
#[test]
fn hebrew_and_islamic_overlays() {
let d = day(2007, 9, 13);
let h = d.alt_hebrew.expect("hebrew overlay");
assert_eq!((h.year, h.month, h.day), (5768, 1, 1));
let i = d.alt_islamic.expect("islamic overlay");
assert_eq!((i.year, i.month, i.day), (1428, 9, 1));
}
#[cfg(feature = "lunisolar")]
#[test]
fn moon_phase_overlay() {
let full = day(2024, 9, 18).moon.expect("moon overlay");
assert_eq!(full.phase_index, 4);
assert_eq!(full.phase_name, "Full Moon");
assert!(
full.illuminated_fraction > 0.98,
"illum {}",
full.illuminated_fraction
);
let new = day(2024, 4, 8).moon.expect("moon overlay");
assert_eq!(new.phase_index, 0);
assert!(
new.illuminated_fraction < 0.02,
"illum {}",
new.illuminated_fraction
);
}
#[cfg(feature = "lunisolar")]
mod seasons {
use timeglyph::cal::{season_for, season_markers, Hemisphere};
#[test]
fn markers_land_on_the_known_solstice_equinox_dates_2026() {
let m = season_markers(2026);
assert_eq!(m[0].solar_longitude_deg, 0.0);
assert!(
m[0].instant_utc.starts_with("2026-03-20"),
"{}",
m[0].instant_utc
);
assert_eq!(m[0].term_name, "春分");
assert!(
m[1].instant_utc.starts_with("2026-06-21"),
"{}",
m[1].instant_utc
);
assert_eq!(m[1].term_name, "夏至");
assert!(
m[2].instant_utc.starts_with("2026-09-23"),
"{}",
m[2].instant_utc
);
assert!(
m[3].instant_utc.starts_with("2026-12-21"),
"{}",
m[3].instant_utc
);
assert_eq!(m[3].term_name, "冬至");
}
#[test]
fn hemisphere_maps_events_to_opposite_seasons() {
assert_eq!(season_for(0.0, Hemisphere::North), "spring");
assert_eq!(season_for(0.0, Hemisphere::South), "autumn");
assert_eq!(season_for(90.0, Hemisphere::North), "summer");
assert_eq!(season_for(270.0, Hemisphere::North), "winter");
assert_eq!(season_for(270.0, Hemisphere::South), "summer");
}
#[test]
fn calday_carries_hemisphere_neutral_solar_longitude() {
use timeglyph::cal::build_day;
use timeglyph::RenderZone;
let d = build_day(jiff::civil::date(2026, 3, 20), &RenderZone::Utc).unwrap();
let lon = d.solar_longitude_deg.expect("solar longitude");
assert!(!(1.0..=359.0).contains(&lon), "lon {lon}");
}
}
#[test]
fn build_month_lays_out_the_grid() {
use timeglyph::cal::{build_month, WeekStart};
let m = build_month(2026, 7, &RenderZone::Utc, WeekStart::Monday).unwrap();
assert_eq!(m.days.len(), 31);
assert_eq!(m.weeks[0][0], None);
assert_eq!(m.weeks[0][1], None);
assert_eq!(m.weeks[0][2], Some(0)); assert_eq!(m.weeks[0][6], Some(4)); }
#[test]
fn render_month_text_shows_header_gutter_and_markers() {
use timeglyph::cal::{build_month, WeekStart};
use timeglyph::cal_render::render_month_text;
let m = build_month(2026, 7, &RenderZone::Utc, WeekStart::Monday).unwrap();
let s = render_month_text(&m, None);
assert!(s.contains("July 2026"), "{s}");
assert!(s.contains("Mo") && s.contains("Su"));
assert!(s.contains("W27")); assert!(
!s.chars().any(|c| ('\u{2500}'..='\u{257F}').contains(&c)),
"box-drawing found"
);
#[cfg(feature = "leap")]
{
let dec = build_month(2016, 12, &RenderZone::Utc, WeekStart::Monday).unwrap();
assert!(render_month_text(&dec, None).contains('+'));
}
}
#[cfg(feature = "lunisolar")]
mod art {
use timeglyph::cal_art::{moon_art, PHASE_GLYPH};
#[test]
fn moon_art_has_eight_discs_of_equal_height() {
for idx in 0u8..8 {
let a = moon_art(idx);
assert_eq!(a.len(), 7, "phase {idx} art height");
}
assert!(moon_art(4).iter().any(|l| l.contains('@')));
assert!(!moon_art(0).iter().any(|l| l.contains('@')));
assert_eq!(PHASE_GLYPH.len(), 8);
}
#[test]
fn day_card_shows_the_moon_disc_on_a_full_moon() {
use timeglyph::cal::build_day;
use timeglyph::cal_render::render_day_text;
use timeglyph::RenderZone;
let d = build_day(jiff::civil::date(2024, 9, 18), &RenderZone::Utc).unwrap();
let card = render_day_text(&d);
assert!(
card.contains('@'),
"full-moon day card should show a lit disc:\n{card}"
);
}
}
#[cfg(feature = "lunisolar")]
#[test]
fn season_strip_shows_boundaries_and_seasons() {
use timeglyph::cal::{season_markers, Hemisphere};
use timeglyph::cal_art::season_strip;
let s = season_strip(2026, &season_markers(2026), Hemisphere::North);
assert!(
s.contains("Spring") && s.contains("Summer") && s.contains("Winter"),
"{s}"
);
assert!(s.contains("2026"), "{s}");
assert!(
!s.chars().any(|c| ('\u{2500}'..='\u{257F}').contains(&c)),
"box-drawing"
);
let south = season_strip(2026, &season_markers(2026), Hemisphere::South);
assert!(south.contains("Summer"), "{south}");
}
#[test]
fn grid_markers_for_epoch_and_rollover_days() {
use timeglyph::cal::{build_month, WeekStart};
use timeglyph::cal_render::render_month_text;
let jan1601 = build_month(1601, 1, &RenderZone::Utc, WeekStart::Sunday).unwrap();
assert!(render_month_text(&jan1601, None).contains('e'));
let jan2038 = build_month(2038, 1, &RenderZone::Utc, WeekStart::Monday).unwrap();
assert!(render_month_text(&jan2038, None).contains('~'));
let d = jiff::civil::date(2038, 1, 19);
assert!(render_month_text(&jan2038, Some(d)).contains('*'));
}
#[test]
fn build_month_rejects_an_invalid_month() {
use timeglyph::cal::{build_month, WeekStart};
assert!(build_month(2026, 13, &RenderZone::Utc, WeekStart::Monday).is_err());
}
#[cfg(feature = "lunisolar")]
#[test]
fn season_tiles_are_five_lines_each() {
use timeglyph::cal_art::season_tile;
for idx in 0u8..4 {
assert_eq!(season_tile(idx).len(), 6, "season {idx}");
}
assert!(season_tile(3).iter().any(|l| l.contains('*'))); }
#[cfg(all(feature = "lunisolar", feature = "altcal"))]
#[test]
fn day_card_shows_all_alternative_calendars_and_season() {
use timeglyph::cal::build_day;
use timeglyph::cal_render::render_day_text;
let card = render_day_text(&build_day(date(2024, 9, 18), &RenderZone::Utc).unwrap());
assert!(
card.contains("年月日時"),
"four-pillar block missing:\n{card}"
);
assert!(card.contains("白露"), "solar term missing:\n{card}");
assert!(card.contains("5784"), "hebrew year missing:\n{card}");
assert!(card.contains("Elul"), "hebrew month name missing:\n{card}");
assert!(card.contains("1446"), "islamic year missing:\n{card}");
assert!(card.contains("summer"), "season missing:\n{card}");
assert!(card.contains("beach"), "summer scene tile missing:\n{card}");
let autumn = render_day_text(&build_day(date(2024, 11, 1), &RenderZone::Utc).unwrap());
assert!(
autumn.contains("autumn"),
"autumn season missing:\n{autumn}"
);
let winter = render_day_text(&build_day(date(2025, 1, 15), &RenderZone::Utc).unwrap());
assert!(
winter.contains("snowman"),
"winter scene tile missing:\n{winter}"
);
}
#[cfg(feature = "lunisolar")]
#[test]
fn month_view_has_an_overlay_footer() {
use timeglyph::cal::{build_month, WeekStart};
use timeglyph::cal_render::render_month_text;
let m = build_month(2026, 7, &RenderZone::Utc, WeekStart::Monday).unwrap();
let s = render_month_text(&m, None);
assert!(
s.contains("年"),
"chinese year pillar missing from month footer:\n{s}"
);
assert!(
s.contains("summer"),
"season missing from month footer:\n{s}"
);
}
#[cfg(feature = "lunisolar")]
#[test]
fn day_card_shows_lunar_chinese_date_and_four_pillars() {
use timeglyph::cal::build_day;
use timeglyph::cal_render::render_day_text;
let card = render_day_text(&build_day(date(2025, 2, 19), &RenderZone::Utc).unwrap());
assert!(
card.contains("正月廿二"),
"lunar Chinese date missing:\n{card}"
);
assert!(card.contains("雨水"), "solar term missing:\n{card}");
assert!(card.contains("乙戊己庚"), "stems row missing:\n{card}");
assert!(card.contains("巳寅未午"), "branches row missing:\n{card}");
assert!(card.contains("年月日時"), "pillar labels missing:\n{card}");
}
#[cfg(feature = "lunisolar")]
#[test]
fn day_card_shows_solar_term_days_into_term() {
use timeglyph::cal::build_day;
use timeglyph::cal_render::render_day_text;
let card = render_day_text(&build_day(date(2025, 2, 25), &RenderZone::Utc).unwrap());
assert!(
card.contains("雨水後第七日"),
"term-phrase missing:\n{card}"
);
let d0 = render_day_text(&build_day(date(2024, 12, 21), &RenderZone::Utc).unwrap());
assert!(
d0.contains("冬至") && !d0.contains("冬至後"),
"bare term missing:\n{d0}"
);
}
#[cfg(feature = "lunisolar")]
#[test]
fn build_day_at_moves_only_the_hour_pillar() {
use jiff::civil::datetime;
use timeglyph::cal::build_day_at;
let zone = RenderZone::parse("+08:00").unwrap();
let at = |h, m| {
build_day_at(datetime(2025, 2, 19, h, m, 0, 0), &zone)
.unwrap()
.alt_chinese
.unwrap()
};
let midnight = at(0, 30); let afternoon = at(14, 30); assert_eq!(
midnight.day_pillar, afternoon.day_pillar,
"day pillar must not move"
);
assert_eq!(midnight.hour_pillar, "甲子", "子時 hour pillar");
assert_eq!(afternoon.hour_pillar, "辛未", "未時 hour pillar");
let noon = build_day_at(datetime(2025, 2, 19, 12, 0, 0, 0), &zone).unwrap();
let default = build_day(date(2025, 2, 19), &zone).unwrap();
assert_eq!(
noon.alt_chinese.unwrap().hour_pillar,
default.alt_chinese.unwrap().hour_pillar
);
}
#[cfg(feature = "lunisolar")]
#[test]
fn hemisphere_and_season_are_derived_from_the_zone() {
use timeglyph::cal::{hemisphere_for, Hemisphere};
let syd = RenderZone::parse("Australia/Sydney").unwrap();
assert_eq!(hemisphere_for(&syd), Hemisphere::South);
let s = build_day(date(2026, 1, 15), &syd).unwrap();
assert_eq!(s.season.as_deref(), Some("summer"));
assert!(s.southern_hemisphere);
let ldn = RenderZone::parse("Europe/London").unwrap();
assert_eq!(hemisphere_for(&ldn), Hemisphere::North);
let n = build_day(date(2026, 1, 15), &ldn).unwrap();
assert_eq!(n.season.as_deref(), Some("winter"));
assert!(!n.southern_hemisphere);
assert_eq!(hemisphere_for(&RenderZone::Utc), Hemisphere::North);
assert_eq!(
hemisphere_for(&RenderZone::parse("+11:00").unwrap()),
Hemisphere::North
);
}
#[cfg(feature = "lunisolar")]
#[test]
fn day_card_season_and_tile_follow_the_zone() {
use timeglyph::cal_render::render_day_text;
let syd = RenderZone::parse("Australia/Sydney").unwrap();
let card = render_day_text(&build_day(date(2026, 1, 15), &syd).unwrap());
assert!(card.contains("summer"), "sydney summer:\n{card}");
assert!(card.contains("beach"), "sydney beach tile:\n{card}");
let ldn = RenderZone::parse("Europe/London").unwrap();
let cardn = render_day_text(&build_day(date(2026, 1, 15), &ldn).unwrap());
assert!(
cardn.contains("winter") && cardn.contains("snowman"),
"london winter:\n{cardn}"
);
}