use std::fmt;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct DateTime {
micros: i64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError(&'static str);
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid date-time: {}", self.0)
}
}
impl std::error::Error for ParseError {}
impl DateTime {
pub const UNIX_EPOCH: DateTime = DateTime { micros: 0 };
pub fn now() -> Self {
let d = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
DateTime {
micros: d.as_micros() as i64,
}
}
pub const fn from_unix_micros(micros: i64) -> Self {
DateTime { micros }
}
pub const fn from_unix_seconds(secs: i64) -> Self {
DateTime {
micros: secs * 1_000_000,
}
}
pub const fn unix_micros(&self) -> i64 {
self.micros
}
pub const fn unix_seconds(&self) -> i64 {
self.micros.div_euclid(1_000_000)
}
pub fn add_seconds(&self, secs: i64) -> Self {
DateTime {
micros: self.micros + secs * 1_000_000,
}
}
pub fn from_utc_fields(
year: i64,
month: u32,
day: u32,
hour: u32,
min: u32,
sec: u32,
micros: u32,
) -> Option<Self> {
if !(1..=12).contains(&month)
|| day == 0
|| day > days_in_month(year, month)
|| hour > 23
|| min > 59
|| sec > 60
|| micros > 999_999
{
return None;
}
let days = days_from_civil(year, month, day);
let secs = days * 86_400 + hour as i64 * 3_600 + min as i64 * 60 + sec as i64;
Some(DateTime {
micros: secs * 1_000_000 + micros as i64,
})
}
pub fn to_utc_fields(&self) -> (i64, u32, u32, u32, u32, u32, u32) {
let secs = self.micros.div_euclid(1_000_000);
let micros = self.micros.rem_euclid(1_000_000) as u32;
let days = secs.div_euclid(86_400);
let sod = secs.rem_euclid(86_400) as u32;
let (y, m, d) = civil_from_days(days);
(y, m, d, sod / 3_600, (sod % 3_600) / 60, sod % 60, micros)
}
pub fn parse(s: &str) -> Result<Self, ParseError> {
let b = s.trim().as_bytes();
let mut p = Cursor { b, i: 0 };
let year = p.digits(4).ok_or(ParseError("year"))? as i64;
p.expect(b'-')
.ok_or(ParseError("expected '-' after year"))?;
let month = p.digits(2).ok_or(ParseError("month"))?;
p.expect(b'-')
.ok_or(ParseError("expected '-' after month"))?;
let day = p.digits(2).ok_or(ParseError("day"))?;
match p.next() {
Some(b'T' | b't' | b' ') => {}
_ => return Err(ParseError("expected 'T' separator")),
}
let hour = p.digits(2).ok_or(ParseError("hour"))?;
p.expect(b':')
.ok_or(ParseError("expected ':' after hour"))?;
let min = p.digits(2).ok_or(ParseError("minute"))?;
let sec = if p.peek() == Some(b':') {
p.i += 1;
p.digits(2).ok_or(ParseError("second"))?
} else {
0
};
let mut micros = 0u32;
if matches!(p.peek(), Some(b'.' | b',')) {
p.i += 1;
let start = p.i;
let mut scale = 100_000u32;
while let Some(c @ b'0'..=b'9') = p.peek() {
micros += (c - b'0') as u32 * scale;
scale /= 10;
p.i += 1;
}
if p.i == start {
return Err(ParseError("empty fraction"));
}
}
let offset_secs: i64 = match p.next() {
None => 0,
Some(b'Z' | b'z') => 0,
Some(sign @ (b'+' | b'-')) => {
let oh = p.digits(2).ok_or(ParseError("offset hour"))? as i64;
if p.peek() == Some(b':') {
p.i += 1;
}
let om = if p.peek().is_some() {
p.digits(2).ok_or(ParseError("offset minute"))? as i64
} else {
0
};
if oh > 23 || om > 59 {
return Err(ParseError("offset out of range"));
}
let o = oh * 3_600 + om * 60;
if sign == b'+' { o } else { -o }
}
Some(_) => return Err(ParseError("unexpected character")),
};
if p.i != b.len() {
return Err(ParseError("trailing characters"));
}
let local = DateTime::from_utc_fields(year, month, day, hour, min, sec, micros)
.ok_or(ParseError("field out of range"))?;
Ok(DateTime {
micros: local.micros - offset_secs * 1_000_000,
})
}
}
impl fmt::Display for DateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (y, mo, d, h, mi, s, us) = self.to_utc_fields();
write!(
f,
"{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}.{:03}Z",
us / 1_000
)
}
}
impl fmt::Debug for DateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl std::str::FromStr for DateTime {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
DateTime::parse(s)
}
}
impl serde::Serialize for DateTime {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(self)
}
}
impl<'de> serde::Deserialize<'de> for DateTime {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct V;
impl serde::de::Visitor<'_> for V {
type Value = DateTime;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("an RFC 3339 date-time string")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<DateTime, E> {
DateTime::parse(v)
.map_err(|_| E::invalid_value(serde::de::Unexpected::Str(v), &self))
}
}
d.deserialize_str(V)
}
}
struct Cursor<'a> {
b: &'a [u8],
i: usize,
}
impl Cursor<'_> {
fn peek(&self) -> Option<u8> {
self.b.get(self.i).copied()
}
fn next(&mut self) -> Option<u8> {
let c = self.peek()?;
self.i += 1;
Some(c)
}
fn expect(&mut self, c: u8) -> Option<()> {
(self.next()? == c).then_some(())
}
fn digits(&mut self, n: usize) -> Option<u32> {
let mut v = 0u32;
for _ in 0..n {
let c = self.next()?;
if !c.is_ascii_digit() {
return None;
}
v = v * 10 + (c - b'0') as u32;
}
Some(v)
}
}
fn is_leap(y: i64) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}
fn days_in_month(y: i64, m: u32) -> u32 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap(y) => 29,
2 => 28,
_ => 0,
}
}
fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = y.div_euclid(400);
let yoe = y - era * 400;
let m = m as i64;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d as i64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
(if m <= 2 { y + 1 } else { y }, m, d)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_and_formats() {
let t = DateTime::parse("2013-02-01T20:53:32.486Z").unwrap();
assert_eq!(t.to_string(), "2013-02-01T20:53:32.486Z");
assert_eq!(t.unix_seconds(), 1_359_752_012);
let o = DateTime::parse("2013-02-01T22:53:32.486+02:00").unwrap();
assert_eq!(t, o);
assert_eq!(
DateTime::parse("2013-02-01T20:53:32")
.unwrap()
.unix_seconds(),
1_359_752_012
);
assert_eq!(
DateTime::parse("2013-02-01t20:53:32.123456789z")
.unwrap()
.unix_micros()
% 1_000_000,
123_456
);
assert_eq!(
DateTime::parse("1970-01-01T00:00:00Z").unwrap(),
DateTime::UNIX_EPOCH
);
assert_eq!(
DateTime::parse("2024-02-29T00:00:00Z").unwrap().to_string(),
"2024-02-29T00:00:00.000Z"
);
assert_eq!(
DateTime::parse("1969-12-31T23:59:59Z")
.unwrap()
.unix_seconds(),
-1
);
assert_eq!(DateTime::parse("2013-02-01 21:53:32.486+01").unwrap(), t);
}
#[test]
fn rejects_garbage() {
for s in [
"",
"2023-13-01T00:00:00Z",
"2023-02-29T00:00:00Z",
"2023-01-01",
"2023-01-01T00:00:00Zjunk",
"2023-01-01T00:00:00.Z",
] {
assert!(DateTime::parse(s).is_err(), "{s}");
}
}
#[test]
fn calendar_consistency() {
for days in (-800_000i64..800_000).step_by(997) {
let (y, m, d) = civil_from_days(days);
assert_eq!(days_from_civil(y, m, d), days);
}
}
}