#[cfg(test)]
mod tests {
use cmn::datetime::{DateTime, Duration, ParseError};
#[test]
fn parse_utc_basic() {
let dt =
DateTime::parse("2026-04-05T14:30:00Z").unwrap();
assert_eq!(dt.year(), 2026);
assert_eq!(dt.month(), 4);
assert_eq!(dt.day(), 5);
assert_eq!(dt.hour(), 14);
assert_eq!(dt.minute(), 30);
assert_eq!(dt.second(), 0);
assert_eq!(dt.offset_minutes(), 0);
}
#[test]
fn parse_positive_offset() {
let dt = DateTime::parse(
"2026-01-15T09:00:00+05:30",
)
.unwrap();
assert_eq!(dt.offset_minutes(), 330);
}
#[test]
fn parse_negative_offset() {
let dt = DateTime::parse(
"2026-12-31T23:59:59-08:00",
)
.unwrap();
assert_eq!(dt.offset_minutes(), -480);
assert_eq!(dt.hour(), 23);
assert_eq!(dt.second(), 59);
}
#[test]
fn parse_midnight() {
let dt =
DateTime::parse("2000-01-01T00:00:00Z").unwrap();
assert_eq!(dt.hour(), 0);
assert_eq!(dt.minute(), 0);
assert_eq!(dt.second(), 0);
}
#[test]
fn parse_leap_day() {
let dt =
DateTime::parse("2024-02-29T12:00:00Z").unwrap();
assert_eq!(dt.month(), 2);
assert_eq!(dt.day(), 29);
}
#[test]
fn parse_epoch() {
let dt =
DateTime::parse("1970-01-01T00:00:00Z").unwrap();
assert_eq!(dt.to_unix_timestamp(), 0);
}
#[test]
fn parse_too_short() {
assert_eq!(
DateTime::parse("2026-04-05"),
Err(ParseError::InvalidFormat)
);
}
#[test]
fn parse_no_timezone() {
assert_eq!(
DateTime::parse("2026-04-05T14:30:00"),
Err(ParseError::InvalidFormat)
);
}
#[test]
fn parse_bad_separator() {
assert_eq!(
DateTime::parse("2026-04-05 14:30:00Z"),
Err(ParseError::InvalidFormat)
);
}
#[test]
fn parse_month_13() {
assert_eq!(
DateTime::parse("2026-13-01T00:00:00Z"),
Err(ParseError::OutOfRange)
);
}
#[test]
fn parse_day_32() {
assert_eq!(
DateTime::parse("2026-01-32T00:00:00Z"),
Err(ParseError::OutOfRange)
);
}
#[test]
fn parse_feb_29_non_leap() {
assert_eq!(
DateTime::parse("2025-02-29T00:00:00Z"),
Err(ParseError::OutOfRange)
);
}
#[test]
fn parse_hour_24() {
assert_eq!(
DateTime::parse("2026-01-01T24:00:00Z"),
Err(ParseError::OutOfRange)
);
}
#[test]
fn parse_bad_timezone_suffix() {
assert_eq!(
DateTime::parse("2026-01-01T00:00:00X"),
Err(ParseError::InvalidTimezone)
);
}
#[test]
fn parse_empty_string() {
assert_eq!(
DateTime::parse(""),
Err(ParseError::InvalidFormat)
);
}
#[test]
fn parse_garbage() {
assert_eq!(
DateTime::parse("not-a-date-at-all!!!"),
Err(ParseError::InvalidFormat)
);
}
#[test]
fn new_valid() {
assert!(DateTime::new(2026, 4, 5, 14, 30, 0, 0)
.is_some());
}
#[test]
fn new_month_zero() {
assert!(DateTime::new(2026, 0, 1, 0, 0, 0, 0)
.is_none());
}
#[test]
fn new_day_zero() {
assert!(DateTime::new(2026, 1, 0, 0, 0, 0, 0)
.is_none());
}
#[test]
fn new_second_60() {
assert!(DateTime::new(2026, 1, 1, 0, 0, 60, 0)
.is_none());
}
#[test]
fn new_offset_too_large() {
assert!(DateTime::new(2026, 1, 1, 0, 0, 0, 1441)
.is_none());
}
#[test]
fn iso8601_roundtrip_utc() {
let input = "2026-04-05T14:30:00Z";
let dt = DateTime::parse(input).unwrap();
assert_eq!(dt.to_iso8601(), input);
}
#[test]
fn iso8601_roundtrip_positive_offset() {
let input = "2026-04-05T14:30:00+05:30";
let dt = DateTime::parse(input).unwrap();
assert_eq!(dt.to_iso8601(), input);
}
#[test]
fn iso8601_roundtrip_negative_offset() {
let input = "2026-12-31T23:59:59-08:00";
let dt = DateTime::parse(input).unwrap();
assert_eq!(dt.to_iso8601(), input);
}
#[test]
fn display_uses_iso8601() {
let dt =
DateTime::parse("2026-04-05T14:30:00Z").unwrap();
assert_eq!(format!("{dt}"), "2026-04-05T14:30:00Z");
}
#[test]
fn unix_epoch_is_zero() {
let dt =
DateTime::parse("1970-01-01T00:00:00Z").unwrap();
assert_eq!(dt.to_unix_timestamp(), 0);
}
#[test]
fn unix_one_day() {
let dt =
DateTime::parse("1970-01-02T00:00:00Z").unwrap();
assert_eq!(dt.to_unix_timestamp(), 86400);
}
#[test]
fn unix_offset_adjusts() {
let utc =
DateTime::parse("2026-04-05T12:00:00Z").unwrap();
let plus2 = DateTime::parse(
"2026-04-05T14:00:00+02:00",
)
.unwrap();
assert_eq!(
utc.to_unix_timestamp(),
plus2.to_unix_timestamp()
);
}
#[test]
fn duration_two_hours() {
let a =
DateTime::parse("2026-04-05T10:00:00Z").unwrap();
let b =
DateTime::parse("2026-04-05T12:00:00Z").unwrap();
let d = b.duration_since(&a);
assert_eq!(d.whole_seconds(), 7200);
assert_eq!(d.whole_hours(), 2);
}
#[test]
fn duration_negative() {
let a =
DateTime::parse("2026-04-05T12:00:00Z").unwrap();
let b =
DateTime::parse("2026-04-05T10:00:00Z").unwrap();
let d = b.duration_since(&a);
assert!(d.is_negative());
assert_eq!(d.whole_seconds(), -7200);
}
#[test]
fn duration_same_instant() {
let a =
DateTime::parse("2026-04-05T12:00:00Z").unwrap();
let d = a.duration_since(&a);
assert_eq!(d.whole_seconds(), 0);
assert!(!d.is_negative());
}
#[test]
fn duration_cross_day() {
let a =
DateTime::parse("2026-04-05T23:00:00Z").unwrap();
let b =
DateTime::parse("2026-04-06T01:00:00Z").unwrap();
assert_eq!(b.duration_since(&a).whole_hours(), 2);
}
#[test]
fn relative_seconds_ago() {
let now =
DateTime::parse("2026-04-05T12:00:30Z").unwrap();
let then =
DateTime::parse("2026-04-05T12:01:00Z").unwrap();
let rel = now.relative_to(&then);
assert_eq!(rel, "30 seconds ago");
}
#[test]
fn relative_minutes_future() {
let now =
DateTime::parse("2026-04-05T12:05:00Z").unwrap();
let then =
DateTime::parse("2026-04-05T12:00:00Z").unwrap();
let rel = now.relative_to(&then);
assert_eq!(rel, "in 5 minutes");
}
#[test]
fn relative_hours() {
let now =
DateTime::parse("2026-04-05T15:00:00Z").unwrap();
let then =
DateTime::parse("2026-04-05T18:00:00Z").unwrap();
let rel = now.relative_to(&then);
assert_eq!(rel, "3 hours ago");
}
#[test]
fn relative_days() {
let now =
DateTime::parse("2026-04-05T12:00:00Z").unwrap();
let then =
DateTime::parse("2026-04-01T12:00:00Z").unwrap();
let rel = now.relative_to(&then);
assert_eq!(rel, "in 4 days");
}
#[test]
fn relative_just_now() {
let dt =
DateTime::parse("2026-04-05T12:00:00Z").unwrap();
assert_eq!(dt.relative_to(&dt), "just now");
}
#[test]
fn relative_one_minute() {
let a =
DateTime::parse("2026-04-05T12:01:00Z").unwrap();
let b =
DateTime::parse("2026-04-05T12:00:00Z").unwrap();
assert_eq!(a.relative_to(&b), "in 1 minute");
}
#[test]
fn relative_one_hour() {
let a =
DateTime::parse("2026-04-05T13:00:00Z").unwrap();
let b =
DateTime::parse("2026-04-05T12:00:00Z").unwrap();
assert_eq!(a.relative_to(&b), "in 1 hour");
}
#[test]
fn relative_one_day() {
let a =
DateTime::parse("2026-04-06T12:00:00Z").unwrap();
let b =
DateTime::parse("2026-04-05T12:00:00Z").unwrap();
assert_eq!(a.relative_to(&b), "in 1 day");
}
#[test]
fn relative_months() {
let a =
DateTime::parse("2026-07-05T12:00:00Z").unwrap();
let b =
DateTime::parse("2026-04-05T12:00:00Z").unwrap();
assert_eq!(a.relative_to(&b), "in 3 months");
}
#[test]
fn relative_years() {
let a =
DateTime::parse("2028-04-05T12:00:00Z").unwrap();
let b =
DateTime::parse("2026-04-05T12:00:00Z").unwrap();
assert_eq!(a.relative_to(&b), "in 2 years");
}
#[test]
fn duration_from_seconds() {
let d = Duration::from_seconds(90);
assert_eq!(d.whole_seconds(), 90);
assert_eq!(d.whole_minutes(), 1);
assert_eq!(d.whole_hours(), 0);
assert_eq!(d.whole_days(), 0);
}
#[test]
fn duration_abs() {
let d = Duration::from_seconds(-3600);
assert!(d.is_negative());
let a = d.abs();
assert!(!a.is_negative());
assert_eq!(a.whole_seconds(), 3600);
}
#[test]
fn duration_display() {
let d = Duration::from_seconds(3661);
assert_eq!(format!("{d}"), "01:01:01");
}
#[test]
fn duration_display_negative() {
let d = Duration::from_seconds(-90);
assert_eq!(format!("{d}"), "-00:01:30");
}
#[test]
fn datetime_copy_eq() {
let a =
DateTime::parse("2026-04-05T14:30:00Z").unwrap();
let b = a; assert_eq!(a, b);
}
#[test]
fn datetime_ord() {
let a =
DateTime::parse("2026-04-05T14:00:00Z").unwrap();
let b =
DateTime::parse("2026-04-05T15:00:00Z").unwrap();
assert!(a < b);
}
#[test]
fn datetime_debug() {
let dt =
DateTime::parse("2026-04-05T14:30:00Z").unwrap();
let dbg = format!("{:?}", dt);
assert!(dbg.contains("DateTime"));
}
#[test]
fn datetime_serde_roundtrip() {
let original =
DateTime::parse("2026-04-05T14:30:00Z").unwrap();
let json =
serde_json::to_string(&original).unwrap();
let restored: DateTime =
serde_json::from_str(&json).unwrap();
assert_eq!(original, restored);
}
#[test]
fn duration_serde_roundtrip() {
let original = Duration::from_seconds(12345);
let json =
serde_json::to_string(&original).unwrap();
let restored: Duration =
serde_json::from_str(&json).unwrap();
assert_eq!(original, restored);
}
#[test]
fn parse_error_display() {
assert_eq!(
format!("{}", ParseError::InvalidFormat),
"invalid ISO 8601 format"
);
assert_eq!(
format!("{}", ParseError::OutOfRange),
"date/time field out of range"
);
assert_eq!(
format!("{}", ParseError::InvalidTimezone),
"invalid timezone offset"
);
}
#[test]
fn parse_error_is_std_error() {
let e: Box<dyn std::error::Error> =
Box::new(ParseError::InvalidFormat);
assert!(!e.to_string().is_empty());
}
#[test]
fn parse_bad_dash_pos4() {
assert_eq!(
DateTime::parse("2026X04-05T14:30:00Z"),
Err(ParseError::InvalidFormat)
);
}
#[test]
fn parse_bad_dash_pos7() {
assert_eq!(
DateTime::parse("2026-04X05T14:30:00Z"),
Err(ParseError::InvalidFormat)
);
}
#[test]
fn parse_bad_colon_pos13() {
assert_eq!(
DateTime::parse("2026-04-05T14X30:00Z"),
Err(ParseError::InvalidFormat)
);
}
#[test]
fn parse_bad_colon_pos16() {
assert_eq!(
DateTime::parse("2026-04-05T14:30X00Z"),
Err(ParseError::InvalidFormat)
);
}
#[test]
fn parse_z_with_trailing_chars() {
assert_eq!(
DateTime::parse("2026-04-05T14:30:00Zextra"),
Err(ParseError::InvalidFormat)
);
}
#[test]
fn parse_offset_wrong_length() {
assert_eq!(
DateTime::parse("2026-04-05T14:30:00+05"),
Err(ParseError::InvalidFormat)
);
}
#[test]
fn parse_offset_bad_colon() {
assert_eq!(
DateTime::parse("2026-04-05T14:30:00+05X30"),
Err(ParseError::InvalidTimezone)
);
}
#[test]
fn parse_offset_hours_out_of_range() {
assert_eq!(
DateTime::parse("2026-04-05T14:30:00+25:00"),
Err(ParseError::InvalidTimezone)
);
}
#[test]
fn relative_one_month() {
let a =
DateTime::parse("2026-05-06T12:00:00Z").unwrap();
let b =
DateTime::parse("2026-04-05T12:00:00Z").unwrap();
assert_eq!(a.relative_to(&b), "in 1 month");
}
#[test]
fn relative_one_year() {
let a =
DateTime::parse("2027-04-05T12:00:00Z").unwrap();
let b =
DateTime::parse("2026-04-05T12:00:00Z").unwrap();
assert_eq!(a.relative_to(&b), "in 1 year");
}
#[test]
fn century_non_leap() {
assert_eq!(
DateTime::parse("1900-02-29T00:00:00Z"),
Err(ParseError::OutOfRange)
);
}
#[test]
fn quad_century_leap() {
let dt =
DateTime::parse("2000-02-29T00:00:00Z").unwrap();
assert_eq!(dt.day(), 29);
}
#[test]
fn last_day_each_month() {
let cases = [
("2026-01-31T00:00:00Z", 31),
("2026-03-31T00:00:00Z", 31),
("2026-04-30T00:00:00Z", 30),
("2026-05-31T00:00:00Z", 31),
("2026-06-30T00:00:00Z", 30),
("2026-07-31T00:00:00Z", 31),
("2026-08-31T00:00:00Z", 31),
("2026-09-30T00:00:00Z", 30),
("2026-10-31T00:00:00Z", 31),
("2026-11-30T00:00:00Z", 30),
("2026-12-31T00:00:00Z", 31),
];
for (input, expected_day) in &cases {
let dt = DateTime::parse(input).unwrap();
assert_eq!(
dt.day(),
*expected_day,
"Failed for {input}"
);
}
}
}