pub mod calendar;
pub mod error;
pub mod format;
pub mod parse;
pub use error::{Field, FormatError, ParseError, UnixTimeError};
pub use parse::parse_rfc3339;
use std::time::SystemTime;
pub fn now_rfc3339() -> Result<String, FormatError> {
format::to_rfc3339(SystemTime::now())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::time::calendar::{civil_from_days, days_from_civil};
use crate::time::format::to_rfc3339;
use std::time::{Duration, UNIX_EPOCH};
#[test]
fn epoch_day_zero_is_nineteen_seventy() {
assert_eq!(civil_from_days(0), (1970, 1, 1));
assert_eq!(days_from_civil(1970, 1, 1), 0);
}
#[test]
fn civil_roundtrips_across_four_centuries() {
let mut days = -719_468;
while days < 719_468 * 2 {
let (year, month, day) = civil_from_days(days);
assert_eq!(
days_from_civil(year, month, day),
days,
"failed at day {days}"
);
days += 97;
}
}
#[test]
fn epoch_renders_without_a_fraction() -> Result<(), FormatError> {
assert_eq!(
to_rfc3339(UNIX_EPOCH)?,
"1970-01-01T00:00:00Z",
"the Unix epoch must render with a four-digit year and no fraction"
);
Ok(())
}
#[test]
fn nanoseconds_render_at_full_precision() -> Result<(), FormatError> {
let instant = UNIX_EPOCH + Duration::new(0, 123_456_789);
assert_eq!(
to_rfc3339(instant)?,
"1970-01-01T00:00:00.123456789Z",
"nanosecond precision must render all nine digits"
);
Ok(())
}
#[test]
fn short_fractions_are_left_aligned() -> Result<(), FormatError> {
let instant = UNIX_EPOCH + Duration::new(0, 120_000_000);
assert_eq!(
to_rfc3339(instant)?,
"1970-01-01T00:00:00.12Z",
"trailing fractional zeroes must be omitted"
);
Ok(())
}
#[test]
fn a_known_instant_renders_exactly() -> Result<(), FormatError> {
let instant = UNIX_EPOCH + Duration::new(1_700_000_000, 500_000_000);
assert_eq!(
to_rfc3339(instant)?,
"2023-11-14T22:13:20.5Z",
"known Unix seconds must retain the expected UTC civil fields"
);
Ok(())
}
#[test]
fn parse_reverses_render_for_the_current_instant() -> Result<(), Box<dyn std::error::Error>> {
let original = SystemTime::now();
let rendered = to_rfc3339(original)?;
let parsed = parse_rfc3339(&rendered)?;
assert_eq!(
to_rfc3339(parsed)?,
rendered,
"parse and canonical formatting must round-trip the current instant"
);
Ok(())
}
#[test]
fn lowercase_separator_and_zulu_are_accepted() -> Result<(), ParseError> {
let parsed = parse_rfc3339("2023-11-14t22:13:20z")?;
assert_eq!(
parsed,
UNIX_EPOCH + Duration::from_secs(1_700_000_000),
"lowercase t and z must parse to the expected UTC instant"
);
Ok(())
}
#[test]
fn a_numeric_offset_is_folded_into_utc() -> Result<(), ParseError> {
let parsed = parse_rfc3339("2023-11-15T00:13:20+02:00")?;
assert_eq!(
parsed,
UNIX_EPOCH + Duration::from_secs(1_700_000_000),
"numeric offsets must normalize to the expected UTC instant"
);
Ok(())
}
#[test]
fn a_missing_offset_is_refused() {
assert_eq!(
parse_rfc3339("2023-11-14T22:13:20"),
Err(ParseError::MissingOffset { at: 19 })
);
}
#[test]
fn a_non_numeric_field_names_itself() {
assert_eq!(
parse_rfc3339("2023-XX-14T22:13:20Z"),
Err(ParseError::NonDigit {
field: Field::Month,
at: 5,
byte: b'X',
})
);
}
#[test]
fn an_out_of_range_month_is_refused() {
assert_eq!(
parse_rfc3339("2023-13-14T22:13:20Z"),
Err(ParseError::OutOfRange {
field: Field::Month,
value: 13,
min: 1,
max: 12,
at: 5,
})
);
}
#[test]
fn leap_day_parses_only_in_a_leap_year() {
assert!(parse_rfc3339("2024-02-29T12:00:00Z").is_ok());
assert!(matches!(
parse_rfc3339("2023-02-29T12:00:00Z"),
Err(ParseError::OutOfRange {
field: Field::Day,
..
})
));
}
#[test]
fn an_over_wide_fraction_is_refused() {
assert_eq!(
parse_rfc3339("2023-11-14T22:13:20.1234567890Z"),
Err(ParseError::FractionWidth { digits: 10, at: 19 })
);
}
#[test]
fn pre_epoch_instants_are_preserved_not_clamped() -> Result<(), Box<dyn std::error::Error>> {
let stamp = "1969-12-31T23:59:59Z";
let parsed = parse_rfc3339(stamp)?;
assert_eq!(
to_rfc3339(parsed)?,
stamp,
"the pre-epoch whole-second instant must round-trip"
);
Ok(())
}
#[test]
fn pre_epoch_fractions_round_trip() -> Result<(), Box<dyn std::error::Error>> {
let stamp = "1969-12-31T23:59:59.5Z";
let parsed = parse_rfc3339(stamp)?;
assert_eq!(
to_rfc3339(parsed)?,
stamp,
"the pre-epoch fraction must remain normalized without loss"
);
Ok(())
}
#[test]
fn the_instant_type_is_std_system_time() -> Result<(), ParseError> {
let parsed: SystemTime = parse_rfc3339("2026-08-17T00:00:00Z")?;
assert!(parsed > UNIX_EPOCH);
Ok(())
}
#[test]
fn now_is_after_the_start_of_twenty_twenty_six() -> Result<(), ParseError> {
let now = SystemTime::now();
let twenty_twenty_six = parse_rfc3339("2026-01-01T00:00:00Z")?;
assert!(now > twenty_twenty_six);
Ok(())
}
}