#![cfg_attr(not(any(feature = "std", test)), no_std)]
use core::cmp::Ordering;
use core::fmt;
use core::str::FromStr;
#[cfg(feature = "python")]
mod python;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
impl Weekday {
pub fn number_from_monday(self) -> u8 {
match self {
Weekday::Monday => 1,
Weekday::Tuesday => 2,
Weekday::Wednesday => 3,
Weekday::Thursday => 4,
Weekday::Friday => 5,
Weekday::Saturday => 6,
Weekday::Sunday => 7,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DateError {
InvalidDate,
OutOfRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Date {
pub year: i32,
pub month: u8, pub day: u8, }
impl Date {
#[inline]
pub fn from_ymd(year: i32, month: u8, day: u8) -> Result<Self, DateError> {
if !(1..=12).contains(&month) {
return Err(DateError::InvalidDate);
}
let dim = days_in_month(year, month);
if day == 0 || day > dim {
return Err(DateError::InvalidDate);
}
Ok(Date { year, month, day })
}
pub const fn from_ymd_unchecked(year: i32, month: u8, day: u8) -> Self {
debug_assert!(month >= 1 && month <= 12);
debug_assert!(day >= 1 && day <= 31);
Date { year, month, day }
}
#[inline]
pub fn from_days_since_unix_epoch(days: i64) -> Result<Self, DateError> {
const ERAS: i64 = 4_726_498_270;
const D_SHIFT: i64 = 146_097 * ERAS - 719_469;
const Y_SHIFT: i64 = 400 * ERAS - 1;
const C1: u64 = 505_054_698_555_331;
const C2: u64 = 50_504_432_782_230_121;
const C3: u64 = 8_619_973_866_219_416;
let rev: i64 = D_SHIFT - days;
let cen: i64 = (((rev as u64 as u128) * (C1 as u128)) >> 64) as i64;
let jul: i64 = rev + cen - cen / 4;
let num: u128 = (jul as u64 as u128) * (C2 as u128);
let yrs: i64 = Y_SHIFT - ((num >> 64) as i64);
let low: u64 = num as u64;
let ypt: i64 = ((782_432u128 * low as u128) >> 64) as i64;
let bump = ypt < 126_464;
let shift: i64 = if bump { 191_360 } else { 977_792 };
let n: i64 = (yrs.rem_euclid(4)) * 512 + shift - ypt;
let d: i64 = (((((n as u64) & 0xFFFF) as u128) * (C3 as u128)) >> 64) as i64;
let day_i: i64 = d + 1;
let month_i: i64 = n / 65_536;
let year_i: i64 = yrs + if bump { 1 } else { 0 };
if !(i32::MIN as i64..=i32::MAX as i64).contains(&year_i) {
return Err(DateError::OutOfRange);
}
let year = year_i as i32;
let month = month_i as u8;
let day = day_i as u8;
if Date::from_ymd(year, month, day).is_err() {
return Err(DateError::InvalidDate);
}
Ok(Date { year, month, day })
}
#[inline]
pub fn days_since_unix_epoch(self) -> i64 {
days_from_civil(self.year, self.month, self.day)
}
pub fn weekday(self) -> Weekday {
let days = self.days_since_unix_epoch();
let w = days.rem_euclid(7);
match w {
0 => Weekday::Thursday,
1 => Weekday::Friday,
2 => Weekday::Saturday,
3 => Weekday::Sunday,
4 => Weekday::Monday,
5 => Weekday::Tuesday,
6 => Weekday::Wednesday,
_ => unreachable!(),
}
}
pub fn ordinal(self) -> u16 {
let month = self.month;
let day = self.day as u16;
const CUM_DAYS: [u16; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
let mut ord = CUM_DAYS[(month - 1) as usize] + day;
if month > 2 && is_leap_year(self.year) {
ord += 1;
}
ord
}
pub fn add_days(self, days: i64) -> Result<Date, DateError> {
let base = self.days_since_unix_epoch();
Date::from_days_since_unix_epoch(base + days)
}
}
impl PartialOrd for Date {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Date {
fn cmp(&self, other: &Self) -> Ordering {
let days_self = self.days_since_unix_epoch();
let days_other = other.days_since_unix_epoch();
days_self.cmp(&days_other)
}
}
impl fmt::Display for Date {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
}
}
impl FromStr for Date {
type Err = DateError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = s.as_bytes();
if bytes.is_empty() {
return Err(DateError::InvalidDate);
}
let mut start = 0;
if bytes[0] == b'+' || bytes[0] == b'-' {
start = 1;
if start == bytes.len() {
return Err(DateError::InvalidDate);
}
}
let mut first = None;
let mut second = None;
for (i, &b) in bytes.iter().enumerate().skip(start) {
if b == b'-' {
if first.is_none() {
first = Some(i);
} else if second.is_none() {
second = Some(i);
} else {
return Err(DateError::InvalidDate);
}
}
}
let (first, second) = match (first, second) {
(Some(first), Some(second)) => (first, second),
_ => return Err(DateError::InvalidDate),
};
let y = parse_i32_bytes(&bytes[..first]).ok_or(DateError::InvalidDate)?;
let m = parse_u32_bytes(&bytes[first + 1..second], 12).ok_or(DateError::InvalidDate)? as u8;
let d = parse_u32_bytes(&bytes[second + 1..], 31).ok_or(DateError::InvalidDate)? as u8;
Date::from_ymd(y, m, d)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimeError {
InvalidTime,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Time {
pub hour: u8, pub minute: u8, pub second: u8, pub nanosecond: u32, }
impl Time {
#[inline]
pub fn from_hms_nano(
hour: u8,
minute: u8,
second: u8,
nanosecond: u32,
) -> Result<Self, TimeError> {
if hour > 23 || minute > 59 || second > 59 || nanosecond >= 1_000_000_000 {
return Err(TimeError::InvalidTime);
}
Ok(Time {
hour,
minute,
second,
nanosecond,
})
}
#[inline]
pub fn seconds_since_midnight(self) -> u32 {
(self.hour as u32) * 3600 + (self.minute as u32) * 60 + (self.second as u32)
}
#[inline]
pub fn nanos_since_midnight(self) -> u64 {
self.seconds_since_midnight() as u64 * 1_000_000_000 + self.nanosecond as u64
}
#[inline]
pub fn from_seconds_nanos(secs: u32, nanos: u32) -> Result<Self, TimeError> {
if secs >= 86_400 || nanos >= 1_000_000_000 {
return Err(TimeError::InvalidTime);
}
let hour = (secs / 3600) as u8;
let rem = secs % 3600;
let minute = (rem / 60) as u8;
let second = (rem % 60) as u8;
Time::from_hms_nano(hour, minute, second, nanos)
}
}
impl PartialOrd for Time {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Time {
fn cmp(&self, other: &Self) -> Ordering {
let nanos_self = self.nanos_since_midnight();
let nanos_other = other.nanos_since_midnight();
nanos_self.cmp(&nanos_other)
}
}
impl fmt::Display for Time {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.nanosecond == 0 {
write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)
} else {
let mut frac = [b'0'; 9];
let mut ns = self.nanosecond;
for i in (0..9).rev() {
frac[i] = b'0' + (ns % 10) as u8;
ns /= 10;
}
let mut end = 9;
while end > 0 && frac[end - 1] == b'0' {
end -= 1;
}
let frac_str = core::str::from_utf8(&frac[..end]).unwrap_or("0");
write!(
f,
"{:02}:{:02}:{:02}.{}",
self.hour, self.minute, self.second, frac_str
)
}
}
}
impl FromStr for Time {
type Err = TimeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = s.as_bytes();
let (hms_bytes, frac_bytes) = match bytes.iter().position(|&b| b == b'.') {
Some(idx) => (&bytes[..idx], Some(&bytes[idx + 1..])),
None => (bytes, None),
};
let mut first = None;
let mut second = None;
for (i, &b) in hms_bytes.iter().enumerate() {
if b == b':' {
if first.is_none() {
first = Some(i);
} else if second.is_none() {
second = Some(i);
} else {
return Err(TimeError::InvalidTime);
}
}
}
let (first, second) = match (first, second) {
(Some(first), Some(second)) => (first, second),
_ => return Err(TimeError::InvalidTime),
};
let h = parse_u32_bytes(&hms_bytes[..first], 23).ok_or(TimeError::InvalidTime)? as u8;
let m =
parse_u32_bytes(&hms_bytes[first + 1..second], 59).ok_or(TimeError::InvalidTime)? as u8;
let sec =
parse_u32_bytes(&hms_bytes[second + 1..], 59).ok_or(TimeError::InvalidTime)? as u8;
let nanos = if let Some(fr) = frac_bytes {
parse_fraction_nanos(fr).ok_or(TimeError::InvalidTime)?
} else {
0
};
Time::from_hms_nano(h, m, sec, nanos)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Duration {
nanos: i128,
}
impl Duration {
pub const ZERO: Duration = Duration { nanos: 0 };
#[inline(always)]
pub fn seconds(secs: i64) -> Duration {
Duration {
nanos: (secs as i128) * 1_000_000_000,
}
}
pub fn milliseconds(ms: i64) -> Duration {
Duration {
nanos: (ms as i128) * 1_000_000,
}
}
pub fn microseconds(us: i64) -> Duration {
Duration {
nanos: (us as i128) * 1_000,
}
}
pub fn nanoseconds(ns: i128) -> Duration {
Duration { nanos: ns }
}
pub fn total_seconds(self) -> f64 {
self.nanos as f64 / 1_000_000_000.0
}
#[inline(always)]
pub fn total_nanos(self) -> i128 {
self.nanos
}
}
impl core::ops::Add for Duration {
type Output = Duration;
fn add(self, rhs: Duration) -> Duration {
Duration {
nanos: self.nanos + rhs.nanos,
}
}
}
impl core::ops::Sub for Duration {
type Output = Duration;
fn sub(self, rhs: Duration) -> Duration {
Duration {
nanos: self.nanos - rhs.nanos,
}
}
}
impl core::ops::Neg for Duration {
type Output = Duration;
fn neg(self) -> Duration {
Duration { nanos: -self.nanos }
}
}
impl PartialOrd for Duration {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Duration {
fn cmp(&self, other: &Self) -> Ordering {
self.nanos.cmp(&other.nanos)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DateTime {
pub date: Date,
pub time: Time,
}
impl DateTime {
#[inline(always)]
pub fn new(date: Date, time: Time) -> DateTime {
DateTime { date, time }
}
#[inline]
pub fn from_unix_timestamp(secs: i64, nanos: i32) -> Result<DateTime, DateError> {
let mut s = secs as i128;
let mut n = nanos as i128;
s += n.div_euclid(1_000_000_000);
n = n.rem_euclid(1_000_000_000);
let s_i64 = s as i64;
let days = s_i64.div_euclid(86_400);
let secs_of_day = s_i64.rem_euclid(86_400);
let date = Date::from_days_since_unix_epoch(days)?;
let time = Time::from_seconds_nanos(secs_of_day as u32, n as u32)
.map_err(|_| DateError::InvalidDate)?;
Ok(DateTime { date, time })
}
#[inline]
pub fn unix_timestamp(self) -> i64 {
let days = self.date.days_since_unix_epoch();
let day_secs = self.time.seconds_since_midnight() as i64;
days * 86_400 + day_secs
}
#[inline]
pub fn unix_timestamp_nanos(self) -> i128 {
self.unix_timestamp() as i128 * 1_000_000_000 + self.time.nanosecond as i128
}
pub fn add_duration(self, dur: Duration) -> Result<DateTime, DateError> {
let t = self.unix_timestamp_nanos() + dur.total_nanos();
let secs = t.div_euclid(1_000_000_000);
let nanos = t.rem_euclid(1_000_000_000);
DateTime::from_unix_timestamp(secs as i64, nanos as i32)
}
#[inline(always)]
pub fn difference(self, other: DateTime) -> Duration {
Duration::nanoseconds(self.unix_timestamp_nanos() - other.unix_timestamp_nanos())
}
#[cfg(feature = "std")]
pub fn now_utc() -> Result<Self, DateError> {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now();
match now.duration_since(UNIX_EPOCH) {
Ok(dur) => {
DateTime::from_unix_timestamp(dur.as_secs() as i64, dur.subsec_nanos() as i32)
}
Err(e) => {
let dur = e.duration();
let secs = dur.as_secs() as i64;
let nanos = dur.subsec_nanos() as i32;
DateTime::from_unix_timestamp(-secs, -nanos)
}
}
}
}
impl fmt::Display for DateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}T{}Z", self.date, self.time)
}
}
impl FromStr for DateTime {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s
.strip_suffix('Z')
.or_else(|| s.strip_suffix('z'))
.ok_or(())?;
let (date_str, time_str) = s.split_once('T').or_else(|| s.split_once(' ')).ok_or(())?;
let date = date_str.parse::<Date>().map_err(|_| ())?;
let time = time_str.parse::<Time>().map_err(|_| ())?;
Ok(DateTime { date, time })
}
}
impl PartialOrd for DateTime {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for DateTime {
fn cmp(&self, other: &Self) -> Ordering {
self.unix_timestamp_nanos()
.cmp(&other.unix_timestamp_nanos())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UtcOffsetError {
OutOfRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UtcOffset {
seconds: i32,
}
impl UtcOffset {
pub fn from_seconds(seconds: i32) -> Result<Self, UtcOffsetError> {
if !(-86_400..=86_400).contains(&seconds) {
return Err(UtcOffsetError::OutOfRange);
}
Ok(UtcOffset { seconds })
}
pub fn from_hours_minutes(
sign_positive: bool,
hours: u8,
minutes: u8,
) -> Result<Self, UtcOffsetError> {
if hours > 23 || minutes > 59 {
return Err(UtcOffsetError::OutOfRange);
}
let total = (hours as i32) * 3600 + (minutes as i32) * 60;
let total = if sign_positive { total } else { -total };
Self::from_seconds(total)
}
#[inline(always)]
pub fn as_seconds(self) -> i32 {
self.seconds
}
#[inline(always)]
pub fn is_utc(self) -> bool {
self.seconds == 0
}
}
impl PartialOrd for UtcOffset {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for UtcOffset {
fn cmp(&self, other: &Self) -> Ordering {
self.seconds.cmp(&other.seconds)
}
}
impl fmt::Display for UtcOffset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut secs = self.seconds;
let sign = if secs >= 0 { '+' } else { '-' };
if secs < 0 {
secs = -secs;
}
let hours = secs / 3600;
let minutes = (secs % 3600) / 60;
write!(f, "{}{:02}:{:02}", sign, hours, minutes)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OffsetDateTime {
pub utc: DateTime,
pub offset: UtcOffset,
}
impl OffsetDateTime {
pub fn from_utc(utc: DateTime, offset: UtcOffset) -> Self {
OffsetDateTime { utc, offset }
}
pub fn from_local(date: Date, time: Time, offset: UtcOffset) -> Result<Self, DateError> {
let local = DateTime::new(date, time);
let utc = local.add_duration(Duration::seconds(-(offset.as_seconds() as i64)))?;
Ok(OffsetDateTime { utc, offset })
}
pub fn to_local(&self) -> Result<DateTime, DateError> {
self.utc
.add_duration(Duration::seconds(self.offset.as_seconds() as i64))
}
#[inline(always)]
pub fn unix_timestamp(&self) -> i64 {
self.utc.unix_timestamp()
}
#[inline(always)]
pub fn unix_timestamp_nanos(&self) -> i128 {
self.utc.unix_timestamp_nanos()
}
pub fn add_duration(&self, dur: Duration) -> Result<Self, DateError> {
let utc = self.utc.add_duration(dur)?;
Ok(OffsetDateTime {
utc,
offset: self.offset,
})
}
#[inline(always)]
pub fn difference(&self, other: OffsetDateTime) -> Duration {
self.utc.difference(other.utc)
}
}
impl fmt::Display for OffsetDateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let local = self
.to_local()
.expect("OffsetDateTime local representation out of range");
write!(f, "{}T{}", local.date, local.time)?;
if self.offset.is_utc() {
write!(f, "Z")
} else {
write!(f, "{}", self.offset)
}
}
}
impl FromStr for OffsetDateTime {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
let (date_part, rest) = s.split_once('T').or_else(|| s.split_once(' ')).ok_or(())?;
let date: Date = date_part.parse().map_err(|_| ())?;
let (time_part, offset_part) = if rest.ends_with('Z') || rest.ends_with('z') {
(&rest[..rest.len() - 1], "Z")
} else {
let idx = rest.rfind(['+', '-']).ok_or(())?;
(&rest[..idx], &rest[idx..])
};
let time: Time = time_part.parse().map_err(|_| ())?;
let offset = parse_rfc3339_offset(offset_part).map_err(|_| ())?;
OffsetDateTime::from_local(date, time, offset).map_err(|_| ())
}
}
impl PartialOrd for OffsetDateTime {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OffsetDateTime {
fn cmp(&self, other: &Self) -> Ordering {
self.utc.cmp(&other.utc)
}
}
const POW10_U32: [u32; 10] = [
1,
10,
100,
1_000,
10_000,
100_000,
1_000_000,
10_000_000,
100_000_000,
1_000_000_000,
];
fn parse_i32_bytes(bytes: &[u8]) -> Option<i32> {
if bytes.is_empty() {
return None;
}
let mut idx = 0;
let mut neg = false;
match bytes[0] {
b'+' => idx = 1,
b'-' => {
idx = 1;
neg = true;
}
_ => {}
}
if idx == bytes.len() {
return None;
}
let limit: i64 = if neg {
i32::MAX as i64 + 1
} else {
i32::MAX as i64
};
let mut val: i64 = 0;
for &b in &bytes[idx..] {
if !b.is_ascii_digit() {
return None;
}
let digit = (b - b'0') as i64;
if val > limit / 10 || (val == limit / 10 && digit > limit % 10) {
return None;
}
val = val * 10 + digit;
}
if neg {
val = -val;
}
Some(val as i32)
}
fn parse_u32_bytes(bytes: &[u8], max: u32) -> Option<u32> {
if bytes.is_empty() {
return None;
}
let mut val: u32 = 0;
for &b in bytes {
if !b.is_ascii_digit() {
return None;
}
let digit = (b - b'0') as u32;
if val > max / 10 || (val == max / 10 && digit > max % 10) {
return None;
}
val = val * 10 + digit;
}
Some(val)
}
fn parse_fraction_nanos(bytes: &[u8]) -> Option<u32> {
let len = bytes.len();
if len == 0 || len > 9 {
return None;
}
let mut val: u32 = 0;
for &b in bytes {
if !b.is_ascii_digit() {
return None;
}
val = val * 10 + (b - b'0') as u32;
}
let scale = 9 - len;
Some(val * POW10_U32[scale])
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Rfc3339OffsetError {
InvalidFormat,
OutOfRange,
}
pub fn parse_rfc3339_offset(s: &str) -> Result<UtcOffset, Rfc3339OffsetError> {
if s == "Z" || s == "z" {
return UtcOffset::from_seconds(0).map_err(|_| Rfc3339OffsetError::OutOfRange);
}
let bytes = s.as_bytes();
if bytes.len() < 3 {
return Err(Rfc3339OffsetError::InvalidFormat);
}
let sign_positive = match bytes[0] {
b'+' => true,
b'-' => false,
_ => return Err(Rfc3339OffsetError::InvalidFormat),
};
let body = &bytes[1..];
let mut colon = None;
for (idx, &b) in body.iter().enumerate() {
if b == b':' {
colon = Some(idx);
break;
}
}
let (h_bytes, m_bytes) = if let Some(colon_idx) = colon {
let h = &body[..colon_idx];
let m = &body[colon_idx + 1..];
if h.is_empty() || h.len() > 2 || m.len() > 2 {
return Err(Rfc3339OffsetError::InvalidFormat);
}
(h, m)
} else if body.len() == 2 {
(&body[..2], &[][..])
} else if body.len() == 4 {
(&body[..2], &body[2..])
} else {
return Err(Rfc3339OffsetError::InvalidFormat);
};
if h_bytes.len() > 2 || m_bytes.len() > 2 {
return Err(Rfc3339OffsetError::InvalidFormat);
}
let hours = parse_u32_bytes(h_bytes, 99).ok_or(Rfc3339OffsetError::InvalidFormat)? as u8;
let minutes = if m_bytes.is_empty() {
0
} else {
parse_u32_bytes(m_bytes, 99).ok_or(Rfc3339OffsetError::InvalidFormat)? as u8
};
UtcOffset::from_hours_minutes(sign_positive, hours, minutes)
.map_err(|_| Rfc3339OffsetError::OutOfRange)
}
fn is_leap_year(year: i32) -> bool {
let century_candidate = year % 25 == 0;
(year & if century_candidate { 15 } else { 3 }) == 0
}
fn days_in_month(year: i32, month: u8) -> u8 {
if month == 2 {
return if is_leap_year(year) { 29 } else { 28 };
}
if !(1..=12).contains(&month) {
return 0;
}
(month ^ (month >> 3)) | 30
}
#[inline]
fn days_from_civil(y: i32, m: u8, d: u8) -> i64 {
const S: i64 = 5_368_710;
const YEAR_SHIFT: i64 = 400 * S;
const RATA_SHIFT: i64 = 719_468 + 146_097 * S + 1;
let bump = m <= 2;
let year = y as i64 + YEAR_SHIFT - if bump { 1 } else { 0 };
let cent = year / 100;
let phase = if bump { 8_829 } else { -2_919 };
let y_days = year * 365 + year / 4 - cent + cent / 4;
let m_days = (979 * (m as i64) + phase) / 32;
y_days + m_days + d as i64 - RATA_SHIFT
}