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
}
#[cfg(test)]
#[path = "timestamp_tests.rs"]
mod tests;