use std::{mem::MaybeUninit, str::FromStr};
#[cfg(test)]
#[path = "./time_tests.rs"]
mod tests;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Date {
pub year: u16,
pub month: u8,
pub day: u8,
}
impl Date {
pub fn new(year: u16, month: u8, day: u8) -> Option<Date> {
if year > 9999 || month == 0 || month > 12 {
return None;
}
if day == 0 || day > days_in_month(year, month) {
return None;
}
Some(Date { year, month, day })
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TimeOffset {
Z,
Custom {
minutes: i16,
},
}
#[derive(Clone, Copy)]
pub struct Time {
flags: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
pub nanosecond: u32,
}
impl std::fmt::Debug for Time {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Time")
.field("hour", &self.hour)
.field("minute", &self.minute)
.field("second", &self.second)
.field("nanosecond", &self.nanosecond)
.finish()
}
}
impl PartialEq for Time {
fn eq(&self, other: &Self) -> bool {
self.hour == other.hour
&& self.minute == other.minute
&& self.second == other.second
&& self.nanosecond == other.nanosecond
}
}
impl Eq for Time {}
impl Time {
pub fn subsecond_precision(&self) -> u8 {
self.flags >> NANO_SHIFT
}
pub fn has_seconds(&self) -> bool {
self.flags & HAS_SECONDS != 0
}
pub fn new(hour: u8, minute: u8, second: u8, nanosecond: u32) -> Option<Time> {
if hour > 23 || minute > 59 || second > 60 || nanosecond > 999_999_999 {
return None;
}
let mut precision: u8 = if nanosecond == 0 { 0 } else { 9 };
let mut n = nanosecond;
while precision > 0 && n.is_multiple_of(10) {
n /= 10;
precision -= 1;
}
let flags = HAS_SECONDS | (precision << NANO_SHIFT);
Some(Time {
flags,
hour,
minute,
second,
nanosecond,
})
}
}
#[derive(Clone, Copy)]
#[repr(C, align(8))]
pub struct DateTime {
date: Date,
flags: u8,
hour: u8,
minute: u8,
seconds: u8,
nanos: u32,
offset_minutes: i16,
}
impl std::fmt::Debug for DateTime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DateTime")
.field("date", &self.date())
.field("time", &self.time())
.field("offset", &self.offset())
.finish()
}
}
impl PartialEq for DateTime {
fn eq(&self, other: &Self) -> bool {
#[repr(C)]
struct Raw {
header: u64,
offset: u32,
nanos: i16,
}
let rhs = unsafe { &*(self as *const _ as *const Raw) };
let lhs = unsafe { &*(other as *const _ as *const Raw) };
(rhs.header == lhs.header) & (rhs.offset == lhs.offset) & (rhs.nanos == lhs.nanos)
}
}
impl Eq for DateTime {}
const HAS_DATE: u8 = 1 << 0;
const HAS_TIME: u8 = 1 << 1;
const HAS_SECONDS: u8 = 1 << 2;
const NANO_SHIFT: u8 = 4;
fn is_leap_year(year: u16) -> bool {
(((year as u64 * 1073750999) as u32) & 3221352463) <= 126976
}
fn days_in_month(year: u16, month: u8) -> u8 {
const DAYS: [u8; 13] = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if month == 2 && is_leap_year(year) {
29
} else {
DAYS[month as usize]
}
}
#[non_exhaustive]
#[derive(Debug)]
pub enum DateTimeError {
Invalid,
}
impl std::fmt::Display for DateTimeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<DateTimeError as std::fmt::Debug>::fmt(self, f)
}
}
impl std::error::Error for DateTimeError {}
impl FromStr for DateTime {
type Err = DateTimeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
DateTime::munch(s.as_bytes())
.ok()
.filter(|(amount, _)| *amount == s.len())
.map(|(_, dt)| dt)
.ok_or(DateTimeError::Invalid)
}
}
impl DateTime {
pub fn local_date(date: Date) -> DateTime {
DateTime {
date,
flags: HAS_DATE,
hour: 0,
minute: 0,
seconds: 0,
nanos: 0,
offset_minutes: i16::MIN,
}
}
pub fn local_time(time: Time) -> DateTime {
DateTime {
date: Date {
year: 0,
month: 0,
day: 0,
},
flags: HAS_TIME | (time.flags & 0xF4),
hour: time.hour,
minute: time.minute,
seconds: time.second,
nanos: time.nanosecond,
offset_minutes: i16::MIN,
}
}
pub fn local_datetime(date: Date, time: Time) -> DateTime {
DateTime {
date,
flags: HAS_DATE | HAS_TIME | (time.flags & 0xF4),
hour: time.hour,
minute: time.minute,
seconds: time.second,
nanos: time.nanosecond,
offset_minutes: i16::MIN,
}
}
pub fn offset_datetime(date: Date, time: Time, offset: TimeOffset) -> Option<DateTime> {
let offset_minutes = match offset {
TimeOffset::Z => i16::MAX,
TimeOffset::Custom { minutes } => {
if !(-1439..=1439).contains(&minutes) {
return None;
}
minutes
}
};
Some(DateTime {
date,
flags: HAS_DATE | HAS_TIME | (time.flags & 0xF4),
hour: time.hour,
minute: time.minute,
seconds: time.second,
nanos: time.nanosecond,
offset_minutes,
})
}
pub const MAX_FORMAT_LEN: usize = 40;
pub fn time(&self) -> Option<Time> {
if self.flags & HAS_TIME != 0 {
Some(Time {
flags: self.flags,
hour: self.hour,
minute: self.minute,
second: self.seconds,
nanosecond: self.nanos,
})
} else {
None
}
}
pub(crate) fn munch(input: &[u8]) -> Result<(usize, DateTime), &'static str> {
enum State {
Year,
Month,
Day,
Hour,
Minute,
Second,
Frac,
OffHour,
OffMin,
}
let mut n = 0;
while n < input.len() && input[n].is_ascii_digit() {
n += 1;
}
let mut state = match input.get(n) {
Some(b':') if n == 2 => State::Hour,
Some(b'-') if n >= 2 => State::Year,
_ => return Err(""),
};
let mut value = DateTime {
date: Date {
year: 0,
month: 0,
day: 0,
},
flags: 0,
hour: 0,
minute: 0,
seconds: 0,
offset_minutes: i16::MIN,
nanos: 0,
};
let mut current = 0u32;
let mut len = 0u32;
let mut off_sign: i16 = 1;
let mut off_hour: u8 = 0;
let mut i = 0usize;
let valid: bool;
'outer: loop {
let byte = input.get(i).copied().unwrap_or(0);
if byte.is_ascii_digit() {
len += 1;
if len <= 9 {
current = current * 10 + (byte - b'0') as u32;
}
i += 1;
continue;
}
'next: {
match state {
State::Year => {
if len != 4 {
return Err("expected 4-digit year");
}
if byte != b'-' {
return Err("");
}
value.date.year = current as u16;
state = State::Month;
break 'next;
}
State::Month => {
let m = current as u8;
if len != 2 {
return Err("expected 2-digit month");
}
if byte != b'-' {
return Err("");
}
if m < 1 || m > 12 {
return Err("month is out of range");
}
value.date.month = m;
state = State::Day;
break 'next;
}
State::Day => {
let d = current as u8;
if len != 2 {
return Err("expected 2-digit day");
}
if d < 1 || d > days_in_month(value.date.year, value.date.month) {
return Err("day is out of range");
}
value.date.day = d;
value.flags |= HAS_DATE;
if byte == b'T'
|| byte == b't'
|| (byte == b' '
&& input.get(i + 1).is_some_and(|b| b.is_ascii_digit()))
{
state = State::Hour;
break 'next;
} else {
valid = true;
break 'outer;
}
}
State::Hour => {
let h = current as u8;
if len != 2 {
return Err("expected 2-digit hour");
}
if byte != b':' {
return Err("incomplete time");
}
if h > 23 {
return Err("hour is out of range");
}
value.hour = h;
state = State::Minute;
break 'next;
}
State::Minute => {
let m = current as u8;
if len != 2 {
return Err("expected 2-digit minute");
}
if m > 59 {
return Err("minute is out of range");
}
value.minute = m;
value.flags |= HAS_TIME;
if byte == b':' {
state = State::Second;
break 'next;
}
}
State::Second => {
let s = current as u8;
if len != 2 {
return Err("expected 2-digit second");
}
if s > 60 {
return Err("second is out of range");
}
value.seconds = s;
value.flags |= HAS_SECONDS;
if byte == b'.' {
state = State::Frac;
break 'next;
}
}
State::Frac => {
if len == 0 {
return Err("expected fractional digits after decimal point");
}
let digit_count = if len > 9 { 9u8 } else { len as u8 };
let mut nanos = current;
let mut s = digit_count;
while s < 9 {
nanos *= 10;
s += 1;
}
value.nanos = nanos;
value.flags |= digit_count << NANO_SHIFT;
}
State::OffHour => {
let h = current as u8;
if len != 2 {
return Err("expected 2-digit offset hour");
}
if byte != b':' {
return Err("incomplete offset");
}
if h > 23 {
return Err("offset hour is out of range");
}
off_hour = h;
state = State::OffMin;
break 'next;
}
State::OffMin => {
if len != 2 {
return Err("expected 2-digit offset minute");
}
if current > 59 {
return Err("offset minute is out of range");
}
value.offset_minutes = off_sign * (off_hour as i16 * 60 + current as i16);
valid = true;
break 'outer;
}
}
match byte {
b'Z' | b'z' => {
value.offset_minutes = i16::MAX;
i += 1;
valid = true;
break 'outer;
}
b'+' => {
off_sign = 1;
state = State::OffHour;
}
b'-' => {
off_sign = -1;
state = State::OffHour;
}
_ => {
valid = true;
break 'outer;
}
}
}
i += 1;
current = 0;
len = 0;
}
if !valid || (value.flags & HAS_DATE == 0 && value.offset_minutes != i16::MIN) {
return Err("");
}
Ok((i, value))
}
pub fn format<'a>(&self, buf: &'a mut MaybeUninit<[u8; DateTime::MAX_FORMAT_LEN]>) -> &'a str {
#[inline(always)]
fn write_byte(
buf: &mut [MaybeUninit<u8>; DateTime::MAX_FORMAT_LEN],
pos: &mut usize,
b: u8,
) {
buf[*pos].write(b);
*pos += 1;
}
#[inline(always)]
fn write_2(
buf: &mut [MaybeUninit<u8>; DateTime::MAX_FORMAT_LEN],
pos: &mut usize,
val: u8,
) {
buf[*pos].write(b'0' + val / 10);
buf[*pos + 1].write(b'0' + val % 10);
*pos += 2;
}
#[inline(always)]
fn write_4(
buf: &mut [MaybeUninit<u8>; DateTime::MAX_FORMAT_LEN],
pos: &mut usize,
val: u16,
) {
buf[*pos].write(b'0' + (val / 1000) as u8);
buf[*pos + 1].write(b'0' + ((val / 100) % 10) as u8);
buf[*pos + 2].write(b'0' + ((val / 10) % 10) as u8);
buf[*pos + 3].write(b'0' + (val % 10) as u8);
*pos += 4;
}
#[inline(always)]
fn write_frac(
buf: &mut [MaybeUninit<u8>; DateTime::MAX_FORMAT_LEN],
pos: &mut usize,
nanos: u32,
digit_count: u8,
) {
let mut val = nanos;
let mut i: usize = 8;
loop {
buf[*pos + i].write(b'0' + (val % 10) as u8);
val /= 10;
if i == 0 {
break;
}
i -= 1;
}
*pos += digit_count as usize;
}
let buf: &mut [MaybeUninit<u8>; Self::MAX_FORMAT_LEN] = unsafe {
&mut *buf
.as_mut_ptr()
.cast::<[MaybeUninit<u8>; Self::MAX_FORMAT_LEN]>()
};
let mut pos: usize = 0;
if self.flags & HAS_DATE != 0 {
write_4(buf, &mut pos, self.date.year);
write_byte(buf, &mut pos, b'-');
write_2(buf, &mut pos, self.date.month);
write_byte(buf, &mut pos, b'-');
write_2(buf, &mut pos, self.date.day);
if self.flags & HAS_TIME != 0 {
write_byte(buf, &mut pos, b'T');
}
}
if self.flags & HAS_TIME != 0 {
write_2(buf, &mut pos, self.hour);
write_byte(buf, &mut pos, b':');
write_2(buf, &mut pos, self.minute);
write_byte(buf, &mut pos, b':');
write_2(buf, &mut pos, self.seconds);
if self.flags & HAS_SECONDS != 0 {
let digit_count = (self.flags >> NANO_SHIFT) & 0xF;
if digit_count > 0 {
write_byte(buf, &mut pos, b'.');
write_frac(buf, &mut pos, self.nanos, digit_count);
}
}
if self.offset_minutes != i16::MIN {
if self.offset_minutes == i16::MAX {
write_byte(buf, &mut pos, b'Z');
} else {
let (sign, abs) = if self.offset_minutes < 0 {
(b'-', (-self.offset_minutes) as u16)
} else {
(b'+', self.offset_minutes as u16)
};
write_byte(buf, &mut pos, sign);
write_2(buf, &mut pos, (abs / 60) as u8);
write_byte(buf, &mut pos, b':');
write_2(buf, &mut pos, (abs % 60) as u8);
}
}
}
unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(buf.as_ptr().cast(), pos))
}
}
pub fn date(&self) -> Option<Date> {
if self.flags & HAS_DATE != 0 {
Some(self.date)
} else {
None
}
}
pub fn offset(&self) -> Option<TimeOffset> {
match self.offset_minutes {
i16::MAX => Some(TimeOffset::Z),
i16::MIN => None,
minutes => Some(TimeOffset::Custom { minutes }),
}
}
}