use super::civil::{CivilDate, CivilDateTime, datetime_from_epoch_secs, epoch_secs_from_datetime};
use std::time::{SystemTime, UNIX_EPOCH};
const RFC3339_MIN_YEAR: i64 = 0;
const RFC3339_MAX_YEAR: i64 = 9999;
#[must_use]
pub fn format_rfc3339(epoch_secs: i64) -> Option<String> {
format_rfc3339_datetime(datetime_from_epoch_secs(epoch_secs))
}
#[must_use]
pub fn format_compact_utc(epoch_secs: i64) -> Option<String> {
let datetime = datetime_from_epoch_secs(epoch_secs);
(datetime.is_valid() && is_rfc3339_year(datetime.date.year)).then(|| {
format!(
"{:04}{:02}{:02}-{:02}{:02}{:02}",
datetime.date.year,
datetime.date.month,
datetime.date.day,
datetime.hour,
datetime.minute,
datetime.second
)
})
}
#[must_use]
pub fn format_rfc3339_datetime(datetime: CivilDateTime) -> Option<String> {
(datetime.is_valid() && is_rfc3339_year(datetime.date.year)).then(|| format_datetime(datetime))
}
#[must_use]
pub fn parse_rfc3339_utc(s: &str) -> Option<i64> {
epoch_secs_from_datetime(parse_rfc3339_utc_datetime(s)?)
}
#[must_use]
pub fn parse_rfc3339_utc_datetime(s: &str) -> Option<CivilDateTime> {
let bytes = s.as_bytes();
if bytes.len() != 20
|| bytes[4] != b'-'
|| bytes[7] != b'-'
|| !matches!(bytes[10], b'T' | b't')
|| bytes[13] != b':'
|| bytes[16] != b':'
|| !matches!(bytes[19], b'Z' | b'z')
{
return None;
}
let year = parse_4(bytes, 0)?;
let date = CivilDate::new(year, parse_2(bytes, 5)?, parse_2(bytes, 8)?)?;
CivilDateTime::new(
date,
parse_2(bytes, 11)?,
parse_2(bytes, 14)?,
parse_2(bytes, 17)?,
)
}
#[must_use]
pub fn now_epoch_secs() -> Option<i64> {
let secs = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
i64::try_from(secs).ok()
}
#[must_use]
pub fn now_utc() -> Option<CivilDateTime> {
now_epoch_secs().map(datetime_from_epoch_secs)
}
#[must_use]
pub fn now_rfc3339() -> Option<String> {
now_epoch_secs().and_then(format_rfc3339)
}
fn parse_2(bytes: &[u8], start: usize) -> Option<i64> {
let tens = i64::from(digit(bytes[start])?);
let ones = i64::from(digit(bytes[start + 1])?);
Some(tens * 10 + ones)
}
fn parse_4(bytes: &[u8], start: usize) -> Option<i64> {
let thousands = i64::from(digit(bytes[start])?);
let hundreds = i64::from(digit(bytes[start + 1])?);
let tens = i64::from(digit(bytes[start + 2])?);
let ones = i64::from(digit(bytes[start + 3])?);
Some(thousands * 1_000 + hundreds * 100 + tens * 10 + ones)
}
fn digit(byte: u8) -> Option<u8> {
byte.is_ascii_digit().then_some(byte - b'0')
}
fn format_datetime(datetime: CivilDateTime) -> String {
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
datetime.date.year,
datetime.date.month,
datetime.date.day,
datetime.hour,
datetime.minute,
datetime.second
)
}
const fn is_rfc3339_year(year: i64) -> bool {
year >= RFC3339_MIN_YEAR && year <= RFC3339_MAX_YEAR
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn formats_rfc3339() {
assert_eq!(format_rfc3339(0), Some("1970-01-01T00:00:00Z".to_owned()));
assert_eq!(
format_rfc3339(86_399),
Some("1970-01-01T23:59:59Z".to_owned())
);
assert_eq!(
format_rfc3339(1_700_000_000),
Some("2023-11-14T22:13:20Z".to_owned())
);
assert_eq!(
format_rfc3339(951_782_400),
Some("2000-02-29T00:00:00Z".to_owned())
);
assert_eq!(format_rfc3339(-1), Some("1969-12-31T23:59:59Z".to_owned()));
}
#[test]
fn formats_compact_utc() {
assert_eq!(format_compact_utc(0), Some("19700101-000000".to_owned()));
assert_eq!(
format_compact_utc(86_400),
Some("19700102-000000".to_owned())
);
assert_eq!(
format_compact_utc(1_700_000_000),
Some("20231114-221320".to_owned())
);
}
#[test]
fn formats_valid_datetime() {
let datetime = CivilDateTime {
date: CivilDate {
year: 2024,
month: 2,
day: 29,
},
hour: 12,
minute: 34,
second: 56,
};
assert_eq!(
format_rfc3339_datetime(datetime),
Some("2024-02-29T12:34:56Z".to_owned())
);
}
#[test]
fn formats_rfc3339_boundary_years() {
assert_eq!(
format_rfc3339_datetime(CivilDateTime {
date: CivilDate {
year: RFC3339_MIN_YEAR,
month: 1,
day: 1,
},
hour: 0,
minute: 0,
second: 0,
}),
Some("0000-01-01T00:00:00Z".to_owned())
);
assert_eq!(
format_rfc3339_datetime(CivilDateTime {
date: CivilDate {
year: RFC3339_MAX_YEAR,
month: 12,
day: 31,
},
hour: 23,
minute: 59,
second: 59,
}),
Some("9999-12-31T23:59:59Z".to_owned())
);
}
#[test]
fn rejects_invalid_datetime_formatting() {
assert_eq!(
format_rfc3339_datetime(CivilDateTime {
date: CivilDate {
year: 2023,
month: 2,
day: 29,
},
hour: 0,
minute: 0,
second: 0,
}),
None
);
assert_eq!(
format_rfc3339_datetime(CivilDateTime {
date: CivilDate {
year: 10_000,
month: 1,
day: 1,
},
hour: 0,
minute: 0,
second: 0,
}),
None
);
assert_eq!(
format_rfc3339_datetime(CivilDateTime {
date: CivilDate {
year: -1,
month: 1,
day: 1,
},
hour: 0,
minute: 0,
second: 0,
}),
None
);
}
#[test]
fn parses_canonical_utc_rfc3339() {
assert_eq!(parse_rfc3339_utc("1970-01-01T00:00:00Z"), Some(0));
assert_eq!(parse_rfc3339_utc("1969-12-31T23:59:59Z"), Some(-1));
assert_eq!(
parse_rfc3339_utc("2023-11-14T22:13:20Z"),
Some(1_700_000_000)
);
assert_eq!(
parse_rfc3339_utc_datetime("2024-02-29t12:34:56z"),
CivilDateTime::new(
CivilDate {
year: 2024,
month: 2,
day: 29,
},
12,
34,
56,
)
);
}
#[test]
fn rejects_non_canonical_or_invalid_rfc3339() {
assert_eq!(parse_rfc3339_utc("2024-02-29T12:34:56+00:00"), None);
assert_eq!(parse_rfc3339_utc("2024-02-29T12:34:56.000Z"), None);
assert_eq!(parse_rfc3339_utc("2024-02-29T12:34:60Z"), None);
assert_eq!(parse_rfc3339_utc("2023-02-29T12:34:56Z"), None);
assert_eq!(parse_rfc3339_utc("2024-13-01T12:34:56Z"), None);
}
#[test]
fn current_rfc3339_has_expected_shape() {
let now = now_rfc3339().expect("system clock should be after the epoch");
assert_eq!(now.len(), "1970-01-01T00:00:00Z".len());
assert!(now.ends_with('Z'));
assert!(now.contains('T'));
}
}