#[cfg(test)]
mod tests {
use fasttime::{
parse_rfc3339_offset, Date, DateError, DateTime, Duration, OffsetDateTime, Time, TimeError,
UtcOffset, Weekday,
};
#[test]
fn date_epoch_and_neighbors() {
let d0 = Date::from_days_since_unix_epoch(0).unwrap();
assert_eq!(d0.year, 1970);
assert_eq!(d0.month, 1);
assert_eq!(d0.day, 1);
let d1 = Date::from_days_since_unix_epoch(1).unwrap();
assert_eq!(d1.to_string(), "1970-01-02");
let dm1 = Date::from_days_since_unix_epoch(-1).unwrap();
assert_eq!(dm1.to_string(), "1969-12-31");
}
#[test]
fn days_round_trip() {
let cases = [
(1970, 1, 1),
(1970, 1, 2),
(1969, 12, 31),
(2000, 2, 29),
(2000, 3, 1),
(1900, 3, 1),
(2400, 2, 29),
];
for &(y, m, d) in &cases {
let date = Date::from_ymd(y, m, d).unwrap();
let days = date.days_since_unix_epoch();
let round = Date::from_days_since_unix_epoch(days).unwrap();
assert_eq!(date, round);
}
}
#[test]
fn datetime_unix_round_trip() {
let date = Date::from_ymd(2024, 5, 17).unwrap();
let time = Time::from_hms_nano(12, 34, 56, 123_456_789).unwrap();
let dt = DateTime::new(date, time);
let secs = dt.unix_timestamp();
let nanos = dt.time.nanosecond as i32;
let rt = DateTime::from_unix_timestamp(secs, nanos).unwrap();
assert_eq!(dt, rt);
}
#[test]
fn duration_add_sub() {
let date = Date::from_ymd(2020, 1, 1).unwrap();
let time = Time::from_hms_nano(0, 0, 0, 0).unwrap();
let dt = DateTime::new(date, time);
let dur = Duration::seconds(86_400); let dt2 = dt.add_duration(dur).unwrap();
assert_eq!(dt2.date.to_string(), "2020-01-02");
assert_eq!(dt2.time.to_string(), "00:00:00");
let diff = dt2.difference(dt);
assert_eq!(diff, dur);
}
#[test]
fn parse_and_display_basic() {
let d: Date = "2023-11-05".parse().unwrap();
assert_eq!(d.to_string(), "2023-11-05");
let t: Time = "23:59:59.001".parse().unwrap();
assert_eq!(t.to_string(), "23:59:59.001");
let dt: DateTime = "2023-11-05T23:59:59.001Z".parse().unwrap();
assert_eq!(dt.to_string(), "2023-11-05T23:59:59.001Z");
}
#[test]
fn offset_datetime_rfc3339() {
let s = "2023-11-05T23:59:59.5+02:00";
let odt: OffsetDateTime = s.parse().unwrap();
assert_eq!(odt.to_string(), s);
let odt_z: OffsetDateTime = "2023-11-05T23:59:59Z".parse().unwrap();
assert_eq!(odt_z.to_string(), "2023-11-05T23:59:59Z");
}
#[test]
fn date_weekday_and_ordinal() {
let monday = Date::from_ymd(2023, 11, 6).unwrap();
assert_eq!(monday.weekday(), Weekday::Monday);
assert_eq!(monday.ordinal(), 310);
let leap = Date::from_ymd(2020, 3, 1).unwrap();
assert_eq!(leap.weekday(), Weekday::Sunday);
assert_eq!(leap.ordinal(), 61);
}
#[test]
fn time_fractional_and_nanos() {
let t: Time = "12:34:56.123450700".parse().unwrap();
assert_eq!(t.nanosecond, 123_450_700);
assert_eq!(t.to_string(), "12:34:56.1234507");
assert_eq!(t.seconds_since_midnight(), 45_296);
assert_eq!(t.nanos_since_midnight(), 45_296_123_450_700);
}
#[test]
fn time_parse_rejects_invalid_fraction() {
assert!(matches!(
"12:00:00.".parse::<Time>(),
Err(TimeError::InvalidTime)
));
assert!(matches!(
"12:00:00.1234567890".parse::<Time>(),
Err(TimeError::InvalidTime)
));
}
#[test]
fn offset_datetime_local_conversion_and_duration() {
let date = Date::from_ymd(2021, 1, 2).unwrap();
let time = Time::from_hms_nano(3, 4, 5, 999_999_999).unwrap();
let offset = UtcOffset::from_hours_minutes(false, 5, 45).unwrap();
let odt = OffsetDateTime::from_local(date, time, offset).unwrap();
assert_eq!(odt.offset, offset);
let local = odt.to_local().unwrap();
assert_eq!(local.date, date);
assert_eq!(local.time, time);
let later = odt.add_duration(Duration::seconds(30)).unwrap();
assert_eq!(later.difference(odt), Duration::seconds(30));
}
#[test]
fn test_i32_year_limits() {
let max_date = Date::from_ymd(i32::MAX, 12, 31).unwrap();
let max_days = max_date.days_since_unix_epoch();
let round_trip_max = Date::from_days_since_unix_epoch(max_days).unwrap();
assert_eq!(
max_date, round_trip_max,
"Failed round-trip at i32::MAX year"
);
let min_date = Date::from_ymd(i32::MIN, 2, 29).unwrap();
let min_days = min_date.days_since_unix_epoch();
let round_trip_min = Date::from_days_since_unix_epoch(min_days).unwrap();
assert_eq!(
min_date, round_trip_min,
"Failed round-trip at i32::MIN year"
);
}
#[test]
fn test_overflow_protection() {
let max_date = Date::from_ymd(i32::MAX, 12, 31).unwrap();
let days_overflow = max_date.days_since_unix_epoch().checked_add(1).unwrap();
let result = Date::from_days_since_unix_epoch(days_overflow);
assert_eq!(
result,
Err(DateError::OutOfRange),
"Did not catch i32 year overflow"
);
}
#[test]
fn test_gregorian_leap_rules() {
let cases = [
(1900, 2, 28, false), (1900, 3, 1, false), (2000, 2, 29, true), (2100, 2, 28, false), (2024, 2, 29, true), (2023, 2, 28, false), ];
for (y, m, d, _) in cases {
let date = Date::from_ymd(y, m, d).expect("Invalid test date setup");
let days = date.days_since_unix_epoch();
let recovered = Date::from_days_since_unix_epoch(days).unwrap();
assert_eq!(date, recovered, "Failed date: {}-{}-{}", y, m, d);
if m == 2 && d == 28 && !is_leap_year(y) {
let next_day = Date::from_days_since_unix_epoch(days + 1).unwrap();
assert_eq!(next_day.month, 3);
assert_eq!(next_day.day, 1);
}
if m == 2 && d == 28 && is_leap_year(y) {
let next_day = Date::from_days_since_unix_epoch(days + 1).unwrap();
assert_eq!(next_day.month, 2);
assert_eq!(next_day.day, 29);
}
}
}
#[test]
fn test_full_400_year_cycle() {
let start_year = 2000;
let mut days_accum = Date::from_ymd(start_year, 1, 1)
.unwrap()
.days_since_unix_epoch();
for y in start_year..(start_year + 400) {
let leap = is_leap_year(y);
let limit = if leap { 366 } else { 365 };
for _ in 0..limit {
let date = Date::from_days_since_unix_epoch(days_accum).unwrap();
assert_eq!(date.year, y, "Year mismatch at days {}", days_accum);
days_accum += 1;
}
}
}
#[test]
fn test_negative_epoch_crossing() {
let d_zero = Date::from_days_since_unix_epoch(0).unwrap();
assert_eq!(d_zero.year, 1970);
assert_eq!(d_zero.month, 1);
assert_eq!(d_zero.day, 1);
let d_neg = Date::from_days_since_unix_epoch(-1).unwrap();
assert_eq!(d_neg.year, 1969);
assert_eq!(d_neg.month, 12);
assert_eq!(d_neg.day, 31);
let d_neg_deep = Date::from_days_since_unix_epoch(-10000).unwrap();
assert!(d_neg_deep.year < 1970);
}
#[test]
fn matches_std_time_reference() {
use time::OffsetDateTime as StdOffsetDateTime;
let samples: &[(i64, i32)] = &[
(0, 0),
(1, 0),
(-1, 0),
(86_399, 999_999_999),
(-86_400, 0),
(1_234_567_890, 987_654_321),
(-1_234_567_890, 123_456_789),
(50 * 365 * 24 * 60 * 60, 1),
(-50 * 365 * 24 * 60 * 60, -250_000_000),
(5_000, 1_500_000_000),
(-5_000, -1_500_000_000),
];
for &(secs, nanos) in samples {
let fast = DateTime::from_unix_timestamp(secs, nanos).unwrap();
let total = (secs as i128) * 1_000_000_000 + nanos as i128;
let std_dt = StdOffsetDateTime::from_unix_timestamp_nanos(total).unwrap();
assert_eq!(fast.date.year, std_dt.year());
assert_eq!(fast.date.month, u8::from(std_dt.month()));
assert_eq!(fast.date.day, std_dt.day());
assert_eq!(fast.time.hour, std_dt.hour());
assert_eq!(fast.time.minute, std_dt.minute());
assert_eq!(fast.time.second, std_dt.second());
assert_eq!(fast.time.nanosecond, std_dt.nanosecond());
}
}
#[test]
fn rfc3339_offset_variants() {
let with_colon = parse_rfc3339_offset("+02:30").unwrap();
assert_eq!(with_colon.as_seconds(), 2 * 3600 + 30 * 60);
let compact = parse_rfc3339_offset("+0230").unwrap();
assert_eq!(compact.as_seconds(), 2 * 3600 + 30 * 60);
let hour_only = parse_rfc3339_offset("-07").unwrap();
assert_eq!(hour_only.as_seconds(), -7 * 3600);
assert!(parse_rfc3339_offset("invalid").is_err());
}
#[test]
fn leap_year_matches_reference_formula() {
for year in -20_000..=20_000 {
let reference = is_leap_year(year);
let fasttime_accepts_feb_29 = Date::from_ymd(year, 2, 29).is_ok();
assert_eq!(
fasttime_accepts_feb_29, reference,
"Mismatch for year {year}"
);
}
}
#[test]
fn month_lengths_match_reference() {
let years = [
i32::MIN,
-4_000,
-2_100,
-2_000,
-1,
0,
1,
1_900,
2_000,
2_024,
2_100,
4_000,
i32::MAX,
];
for &year in &years {
for month in 1..=12 {
let expected = match month {
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
4 | 6 | 9 | 11 => 30,
_ => 31,
};
assert!(
Date::from_ymd(year, month, expected).is_ok(),
"Expected valid end-of-month for {year:04}-{month:02}"
);
assert!(
Date::from_ymd(year, month, expected + 1).is_err(),
"Expected invalid overflow day for {year:04}-{month:02}"
);
}
}
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
}