use greg::{
ymd_hms,
utc,
Span,
Frame,
Calendar,
real::Scale,
calendar::Weekday
};
use crate::*;
const TEST_ZONES: [Zone; 8] = [
Zone::Europe__Berlin,
Zone::America__New_York,
Zone::Asia__Hong_Kong,
Zone::Australia__Sydney,
Zone::Antarctica__McMurdo,
Zone::Asia__Omsk,
Zone::Iceland,
Zone::MST
];
const ALL_SCALES: [Scale; 7] = [
Scale::Years,
Scale::Months,
Scale::Weeks,
Scale::Days,
Scale::Hours,
Scale::Minutes,
Scale::Seconds
];
const FRAMES_TO_CHECK: usize = 10;
fn floor_check(zone: Zone, case: Point, expected: Point, scale: Scale) {
let cal = Calendar(zone);
let received = cal.date_floor_lossy(scale, case);
assert_eq!(
expected,
received,
"\n{} by {:?} should round down to {} but received {}",
cal.lookup(case),
scale,
cal.lookup(expected),
cal.lookup(received)
);
if scale == Scale::Weeks {
assert_eq!(cal.weekday(received), Weekday::Monday);
}
}
fn fmt_frame<Z: Shift>(cal: &Calendar<Z>, frame: Frame) -> String {
let start = cal.lookup(frame.start);
let stop = cal.lookup(frame.stop);
format!("{start} → {stop}")
}
#[test]
fn cet() {
let dt = ymd_hms!(2020-01-01);
let p = Calendar(Zone::Europe__Berlin).resolve_lossy(dt);
let rev = Calendar(Zone::Europe__Berlin).lookup(p);
assert_eq!(dt, rev);
let utc = Utc::resolve(dt);
let off = utc - p;
assert_eq!(off, Span::from_seconds(60 * 60));
let name = Zone::Europe__Berlin.offset_at(p).name();
assert_eq!(name, "CET");
let dt = ymd_hms!(2022-03-27 02:00:00);
let in_utc = utc!(2022-03-27 01:00:00);
let result = Calendar(Zone::Europe__Berlin).try_resolve(dt);
let skipped = Ambiguity::Skipped(in_utc);
assert_eq!(result, Err(skipped));
}
#[test]
fn cest() {
let dt = ymd_hms!(2020-07-01);
let p = Calendar(Zone::Europe__Berlin).resolve_lossy(dt);
let rev = Calendar(Zone::Europe__Berlin).lookup(p);
assert_eq!(dt, rev);
let utc = Utc::resolve(dt);
let off = utc - p;
assert_eq!(off, Span::from_seconds(2 * 60 * 60));
let name = Zone::Europe__Berlin.offset_at(p).name();
assert_eq!(name, "CEST");
let dt = ymd_hms!(2022-10-30 02:00:00);
let first = utc!(2022-10-30 00:00:00);
let second = utc!(2022-10-30 01:00:00);
let result = Calendar(Zone::Europe__Berlin).try_resolve(dt);
let repeated = Ambiguity::Repeated([
(first, Offset::new("CEST", Span::parse("2h").seconds as i64)),
(second, Offset::new("CET", Span::parse("1h").seconds as i64))
]);
assert_eq!(result, Err(repeated));
}
#[test]
fn date_floor() {
for zone in TEST_ZONES {
let cal = Calendar(zone);
let jan_1: Point = cal.resolve_lossy(ymd_hms!(2020-01-01));
let jan_2: Point = cal.resolve_lossy(ymd_hms!(2020-01-02));
let jan_31: Point = cal.resolve_lossy(ymd_hms!(2020-01-31));
let dec_31: Point = cal.resolve_lossy(ymd_hms!(2020-12-31));
for scale in [Scale::Years, Scale::Months] {
floor_check(zone, jan_1, jan_1, scale);
floor_check(zone, jan_2, jan_1, scale);
floor_check(zone, jan_31, jan_1, scale);
}
floor_check(zone, dec_31, jan_1, Scale::Years);
let jan_1_2018: Point = cal.resolve_lossy(ymd_hms!(2018-01-01));
for scale in ALL_SCALES {
floor_check(zone, jan_1_2018, jan_1_2018, scale);
}
let jan_3: Point = cal.resolve_lossy(ymd_hms!(2022-01-03));
for days in 0..6 {
let weekday = jan_3 + Span::DAY * days;
floor_check(zone, weekday, jan_3, Scale::Weeks);
}
let sep_12: Point = cal.resolve_lossy(ymd_hms!(2022-09-12));
for days in 0..6 {
let weekday = sep_12 + Span::DAY * days;
floor_check(zone, weekday, sep_12, Scale::Weeks);
}
}
}
#[test]
fn frames() {
for zone in TEST_ZONES {
println!("\nZONE: {}\n", zone.name);
let cal = Calendar(zone);
let frames = cal.frames(Scale::Weeks, Point::now());
for f @ Frame {start, stop} in frames.take(FRAMES_TO_CHECK) {
assert_eq!(cal.weekday(start), Weekday::Monday);
assert_eq!(cal.weekday(stop), Weekday::Monday);
let start = cal.lookup(start);
let stop = cal.lookup(stop);
assert_eq!(start.1.as_seconds(), 0);
assert_eq!(stop.1.as_seconds(), 0);
println!("{start} → {stop} ({})", f.span());
}
for scale in ALL_SCALES {
println!("\nScale: {scale:12?} | Start: {}", cal.lookup(Point::now()));
let frames = cal.frames(scale, Point::now());
let iter = frames.take(FRAMES_TO_CHECK).enumerate();
for (i, f @ Frame {start, stop}) in iter {
println!("{i} | {}", fmt_frame(&cal, f));
assert_eq!(
cal.date_floor_lossy(scale, start),
start,
"\nstart isn't date_floored: got {}, date_floor {}",
cal.lookup(start),
cal.lookup(cal.date_floor_lossy(scale, start))
);
assert_eq!(
cal.date_floor_lossy(scale, stop),
stop,
"\nstop isn't date_floored: got {}, date_floor {}",
cal.lookup(stop),
cal.lookup(cal.date_floor_lossy(scale, stop))
);
}
}
}
}
#[test]
fn non_existent() {
struct Case {
zone: Zone,
first: DateTime,
last: DateTime,
skipped: [DateTime; 2],
transition_point: Point
}
let cases = [
Case {
zone: Zone::Europe__Berlin,
first: ymd_hms!(2022-03-27 02:00:00),
last: ymd_hms!(2022-03-27 02:59:59),
skipped: [
ymd_hms!(2022-03-27 02:15:00),
ymd_hms!(2022-03-27 02:30:00),
],
transition_point: utc!(2022-03-27 01:00:00)
},
Case {
zone: Zone::Pacific__Auckland,
first: ymd_hms!(2022-09-25 02:00:00),
last: ymd_hms!(2022-09-25 02:59:59),
skipped: [
ymd_hms!(2022-09-25 02:15:00),
ymd_hms!(2022-09-25 02:30:00),
],
transition_point: utc!(2022-09-24 14:00:00)
},
Case {
zone: Zone::Iceland,
first: ymd_hms!(1912-01-01 00:00:00),
last: ymd_hms!(1912-01-01 00:16:07),
skipped: [
ymd_hms!(1912-01-01 00:00:01),
ymd_hms!(1912-01-01 00:05:30),
],
transition_point: utc!(1912-01-01 00:16:08)
},
Case {
zone: Zone::Pacific__Fakaofo,
first: ymd_hms!(2011-12-30 00:00:00),
last: ymd_hms!(2011-12-30 23:59:59),
skipped: [
ymd_hms!(2011-12-30 10:15:00),
ymd_hms!(2011-12-30 14:30:00),
],
transition_point: utc!(2011-12-30 11:00:00)
}
];
for Case {zone, first, last, skipped, transition_point} in cases {
println!("Zone: {}", zone.name);
let cal = Calendar(zone);
println!(
"Transition Point:\n\t{} UTC\n\t{}\n",
Calendar(Utc).format(transition_point),
zone.format_with_name(transition_point)
);
for &dt in &[first, skipped[0], skipped[1], last] {
println!("\tSkipped local time: {dt}");
let res = cal.resolve_lossy(dt);
println!("\tResolved to local: {}", cal.format(res));
println!("\tWhich is UTC time: {}", Calendar(Utc).format(res));
println!();
assert_eq!(res, transition_point);
let skipped_utc = Utc::resolve(dt);
let skipped_local = cal.0.revert_lossy(skipped_utc);
assert_eq!(skipped_local, transition_point);
assert_eq!(
cal.try_resolve(dt),
Err(Ambiguity::Skipped(transition_point))
);
}
assert_eq!(
cal.0.revert_lossy(Utc::resolve(first) - Span::SECOND),
transition_point - Span::SECOND,
"time is linear before the transition"
);
assert_eq!(
cal.0.revert_lossy(Utc::resolve(last) + Span::SECOND),
transition_point,
"the first point not skipped must be the transition point"
);
assert_eq!(
cal.0.revert_lossy(Utc::resolve(last) + Span::SECOND * 2),
transition_point + Span::SECOND,
"time is linear after the transition"
);
}
}
#[test]
fn ambiguous() {
struct Case {
zone: Zone,
ambiguous: Vec<DateTime>,
before: Offset,
after: Offset
}
let cases = [
Case {
zone: Zone::Europe__Berlin,
ambiguous: vec![
ymd_hms!(2022-10-30 02:00:00),
ymd_hms!(2022-10-30 02:15:00),
ymd_hms!(2022-10-30 02:30:00),
ymd_hms!(2022-10-30 02:59:59)
],
before: Offset::new("CEST", Span::parse("2h").seconds as i64),
after: Offset::new("CET", Span::parse("1h").seconds as i64)
},
Case {
zone: Zone::Pacific__Auckland,
ambiguous: vec![
ymd_hms!(2022-04-03 02:00:00),
ymd_hms!(2022-04-03 02:15:00),
ymd_hms!(2022-04-03 02:30:00),
ymd_hms!(2022-04-03 02:59:59)
],
before: Offset::new("NZDT", Span::parse("13h").seconds as i64),
after: Offset::new("NZST", Span::parse("12h").seconds as i64)
},
Case {
zone: Zone::Pacific__Kwajalein,
ambiguous: vec![
ymd_hms!(1969-09-30 01:00:00),
ymd_hms!(1969-09-30 05:30:00),
ymd_hms!(1969-09-30 15:15:00),
ymd_hms!(1969-09-30 23:59:59)
],
before: Offset::new("+11", Span::parse("11h").seconds as i64),
after: Offset::new("-12", -(Span::parse("12h").seconds as i64))
},
];
for Case {zone, ambiguous, before, after} in cases {
let amount = (before.seconds - after.seconds) as u64;
let amount = Span::from_seconds(amount);
println!("Zone: {}", zone.name);
println!("Ambiguous period: {amount}");
let cal = Calendar(zone);
for &dt in &ambiguous {
println!("\tAmbiguous local time: {dt}");
let res_early = cal.resolve_lossy(dt);
let again_at = res_early + amount;
let repeated = cal.lookup(again_at);
assert_eq!(dt, repeated);
assert_ne!(
zone.format_with_name(res_early),
zone.format_with_name(again_at),
"date & time are supposed to match but not the name"
);
println!("\tOccurred first: {} UTC", Utc::lookup(res_early));
println!("\tThen again at: {} UTC", Utc::lookup(again_at));
println!();
assert_eq!(
cal.try_resolve(dt),
Err(Ambiguity::Repeated([
(res_early, before),
(again_at, after)
]))
)
}
}
}
#[test]
fn offset_short_names() {
let cases = [
(
Zone::Pacific__Chatham,
Point::from_epoch(1712412000),
"+1245"
),
(
Zone::Pacific__Kiritimati,
Point::from_epoch(-2177415040),
"-1040"
),
(
Zone::Etc__GMTMinus3,
Point::from_epoch(0),
"+03"
),
(
Zone::Europe__Berlin,
Point::from_epoch(1737481416),
"CET"
)
];
for (zone, point, name) in cases {
assert_eq!(zone.offset_at(point).name, name);
}
let cases = [
(
Zone::America__Scoresbysund,
ymd_hms!(2024-05-01),
"-01"
),
(
Zone::Antarctica__Casey,
ymd_hms!(2022-11-01),
"+11"
),
(
Zone::Africa__Addis_Ababa,
ymd_hms!(1937-02-01),
"+0245"
),
];
for (zone, date, name) in cases {
let point = Calendar(zone).resolve_lossy(date);
assert_eq!(zone.offset_at(point).name, name);
}
}
#[test]
fn lmt() {
let before = ymd_hms!(1911-12-31 23:59:59);
let before_local = Calendar(Zone::Iceland).resolve_lossy(before);
let before_utc = Calendar(Utc).resolve(before);
assert_eq!(before_utc - before_local, Span::parse("16m8s"));
}
#[test]
#[should_panic]
fn too_early() {
let too_early = ymd_hms!(1799-12-31 23:45:56);
let _invalid = Calendar(Zone::Europe__Berlin).resolve_lossy(too_early);
}
#[test]
#[should_panic]
fn too_late() {
let too_late = ymd_hms!(2121-01-01 01:23:45);
let _invalid = Calendar(Zone::Europe__Berlin).resolve_lossy(too_late);
}