const SECS_PER_DAY: i64 = 86_400;
pub(crate) fn parse_rfc3339(input: &str) -> Option<i64> {
let bytes = input.as_bytes();
if bytes.len() < 20 {
return None;
}
let year: i64 = digits(&input[0..4])?;
if bytes[4] != b'-' {
return None;
}
let month: i64 = digits(&input[5..7])?;
if bytes[7] != b'-' {
return None;
}
let day: i64 = digits(&input[8..10])?;
if !matches!(bytes[10], b'T' | b't' | b' ') {
return None;
}
let hour: i64 = digits(&input[11..13])?;
if bytes[13] != b':' {
return None;
}
let minute: i64 = digits(&input[14..16])?;
if bytes[16] != b':' {
return None;
}
let second: i64 = digits(&input[17..19])?;
if !(1..=12).contains(&month) || day < 1 || hour > 23 || minute > 59 || second > 60 {
return None;
}
if day > days_in_month(year, month) {
return None;
}
let offset = parse_offset(&input[19..])?;
let days = days_from_civil(year, month, day);
let secs = days * SECS_PER_DAY + hour * 3600 + minute * 60 + second;
Some(secs - offset)
}
fn parse_offset(rest: &str) -> Option<i64> {
let bytes = rest.as_bytes();
let mut i = 0;
if bytes.first() == Some(&b'.') {
i = 1;
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
if i == start {
return None;
}
}
match bytes.get(i) {
Some(b'Z' | b'z') if i + 1 == bytes.len() => Some(0),
Some(sign @ (b'+' | b'-')) => {
let zone = rest.get(i + 1..)?;
if zone.len() != 5 || zone.as_bytes()[2] != b':' {
return None;
}
let hours: i64 = digits(&zone[0..2])?;
let minutes: i64 = digits(&zone[3..5])?;
if hours > 23 || minutes > 59 {
return None;
}
let magnitude = hours * 3600 + minutes * 60;
Some(if *sign == b'-' { -magnitude } else { magnitude })
}
_ => None,
}
}
fn digits(s: &str) -> Option<i64> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
s.parse().ok()
}
const fn is_leap_year(year: i64) -> bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
const fn days_in_month(year: i64, month: i64) -> i64 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap_year(year) => 29,
2 => 28,
_ => 0,
}
}
fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
let year = year - i64::from(month <= 2);
let era = if year >= 0 { year } else { year - 399 } / 400;
let year_of_era = year - era * 400;
let month_shifted = if month > 2 { month - 3 } else { month + 9 };
let day_of_year = (153 * month_shifted + 2) / 5 + day - 1;
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
era * 146_097 + day_of_era - 719_468
}
fn civil_from_days(days: i64) -> (i64, i64, i64) {
let shifted = days + 719_468;
let era = shifted.div_euclid(146_097);
let day_of_era = shifted.rem_euclid(146_097);
let year_of_era =
(day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let year = year_of_era + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let month_shifted = (5 * day_of_year + 2) / 153;
let day = day_of_year - (153 * month_shifted + 2) / 5 + 1;
let month = if month_shifted < 10 {
month_shifted + 3
} else {
month_shifted - 9
};
let year = if month <= 2 { year + 1 } else { year };
(year, month, day)
}
pub(crate) fn format_local(unix_secs: i64) -> String {
render_with_offset(unix_secs, local_offset_seconds(unix_secs))
}
fn render_with_offset(unix_secs: i64, offset: Option<i64>) -> String {
let Some(offset) = offset else {
return format!("{}Z", format_civil(unix_secs));
};
let sign = if offset < 0 { '-' } else { '+' };
let magnitude = offset.abs();
format!(
"{} {sign}{:02}:{:02}",
format_civil(unix_secs + offset),
magnitude / 3600,
(magnitude % 3600) / 60
)
}
fn format_civil(secs_total: i64) -> String {
let days = secs_total.div_euclid(SECS_PER_DAY);
let secs = secs_total.rem_euclid(SECS_PER_DAY);
let (year, month, day) = civil_from_days(days);
format!(
"{year:04}-{month:02}-{day:02} {:02}:{:02}:{:02}",
secs / 3600,
(secs % 3600) / 60,
secs % 60
)
}
#[cfg(unix)]
#[path = "offset_unix.rs"]
mod offset;
#[cfg(windows)]
#[path = "offset_windows.rs"]
mod offset;
fn local_offset_seconds(unix_secs: i64) -> Option<i64> {
offset::local_offset_seconds(unix_secs)
}
#[cfg(windows)]
fn to_system_time(unix_secs: i64) -> Option<windows_sys::Win32::Foundation::SYSTEMTIME> {
let days = unix_secs.div_euclid(SECS_PER_DAY);
let secs = unix_secs.rem_euclid(SECS_PER_DAY);
let (year, month, day) = civil_from_days(days);
Some(windows_sys::Win32::Foundation::SYSTEMTIME {
wYear: u16::try_from(year).ok()?,
wMonth: u16::try_from(month).ok()?,
wDayOfWeek: 0,
wDay: u16::try_from(day).ok()?,
wHour: u16::try_from(secs / 3600).ok()?,
wMinute: u16::try_from((secs % 3600) / 60).ok()?,
wSecond: u16::try_from(secs % 60).ok()?,
wMilliseconds: 0,
})
}
#[cfg(windows)]
fn from_system_time(t: &windows_sys::Win32::Foundation::SYSTEMTIME) -> i64 {
let days = days_from_civil(i64::from(t.wYear), i64::from(t.wMonth), i64::from(t.wDay));
days * SECS_PER_DAY
+ i64::from(t.wHour) * 3600
+ i64::from(t.wMinute) * 60
+ i64::from(t.wSecond)
}
#[cfg(test)]
mod tests;