use super::components::components_to_timestamp;
use super::components::DateTimeComponents;
use super::civil::days_in_month;
use crate::time_units::TimeUnit;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
InvalidFormat,
InvalidDate { year: i32, month: u32, day: u32 },
InvalidTime { hour: u32, minute: u32, second: u32 },
InvalidTimezone,
InvalidValue,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseError::InvalidFormat => write!(f, "Invalid datetime format"),
ParseError::InvalidDate { year, month, day } => {
write!(f, "Invalid date: {}-{:02}-{:02}", year, month, day)
}
ParseError::InvalidTime { hour, minute, second } => {
write!(f, "Invalid time: {:02}:{:02}:{:02}", hour, minute, second)
}
ParseError::InvalidTimezone => write!(f, "Invalid timezone offset"),
ParseError::InvalidValue => write!(f, "Invalid numeric value"),
}
}
}
impl std::error::Error for ParseError {}
#[inline]
fn parse_u32_fixed(bytes: &[u8], start: usize, width: usize) -> Result<u32, ParseError> {
if start + width > bytes.len() {
return Err(ParseError::InvalidFormat);
}
let mut result = 0u32;
for i in 0..width {
let b = bytes[start + i];
if !b.is_ascii_digit() {
return Err(ParseError::InvalidValue);
}
result = result * 10 + (b - b'0') as u32;
}
Ok(result)
}
#[inline]
fn parse_i32_fixed(bytes: &[u8], start: usize, width: usize) -> Result<i32, ParseError> {
if start + width > bytes.len() {
return Err(ParseError::InvalidFormat);
}
let negative = bytes[start] == b'-';
let start_digit = if negative || bytes[start] == b'+' {
start + 1
} else {
start
};
let mut result = 0i32;
for i in start_digit..(start + width) {
let b = bytes[i];
if !b.is_ascii_digit() {
return Err(ParseError::InvalidValue);
}
result = result * 10 + (b - b'0') as i32;
}
Ok(if negative { -result } else { result })
}
#[inline]
fn validate_date(year: i32, month: u32, day: u32) -> Result<(), ParseError> {
if month < 1 || month > 12 {
return Err(ParseError::InvalidDate { year, month, day });
}
let max_day = days_in_month(year, month);
if day < 1 || day > max_day {
return Err(ParseError::InvalidDate { year, month, day });
}
Ok(())
}
#[inline]
fn validate_time(hour: u32, minute: u32, second: u32) -> Result<(), ParseError> {
if hour >= 24 || minute >= 60 || second >= 60 {
return Err(ParseError::InvalidTime {
hour,
minute,
second,
});
}
Ok(())
}
pub fn parse_iso8601(s: &str) -> Result<(i64, TimeUnit), ParseError> {
let bytes = s.as_bytes();
let len = bytes.len();
if len < 10 {
return Err(ParseError::InvalidFormat);
}
let year = parse_i32_fixed(bytes, 0, 4)?;
if bytes.get(4) != Some(&b'-') {
return Err(ParseError::InvalidFormat);
}
let month = parse_u32_fixed(bytes, 5, 2)?;
if bytes.get(7) != Some(&b'-') {
return Err(ParseError::InvalidFormat);
}
let day = parse_u32_fixed(bytes, 8, 2)?;
validate_date(year, month, day)?;
if len == 10 {
let comp = DateTimeComponents {
year,
month,
day,
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
};
return Ok((components_to_timestamp(&comp, TimeUnit::Seconds), TimeUnit::Seconds));
}
if len < 19 {
return Err(ParseError::InvalidFormat);
}
if bytes.get(10) != Some(&b'T') && bytes.get(10) != Some(&b' ') {
return Err(ParseError::InvalidFormat);
}
let hour = parse_u32_fixed(bytes, 11, 2)?;
if bytes.get(13) != Some(&b':') {
return Err(ParseError::InvalidFormat);
}
let minute = parse_u32_fixed(bytes, 14, 2)?;
if bytes.get(16) != Some(&b':') {
return Err(ParseError::InvalidFormat);
}
let second = parse_u32_fixed(bytes, 17, 2)?;
validate_time(hour, minute, second)?;
let mut nanosecond = 0u32;
let mut time_unit = TimeUnit::Seconds;
let mut pos = 19;
if pos < len && bytes[pos] == b'.' {
pos += 1;
let frac_start = pos;
while pos < len && bytes[pos].is_ascii_digit() {
pos += 1;
}
let frac_width = pos - frac_start;
if frac_width > 0 {
let frac_value = parse_u32_fixed(bytes, frac_start, frac_width)?;
nanosecond = match frac_width {
1 => frac_value * 100_000_000,
2 => frac_value * 10_000_000,
3 => {
time_unit = TimeUnit::Milliseconds;
frac_value * 1_000_000
}
4 => frac_value * 100_000,
5 => frac_value * 10_000,
6 => {
time_unit = TimeUnit::Microseconds;
frac_value * 1_000
}
7 => frac_value * 100,
8 => frac_value * 10,
9 => {
time_unit = TimeUnit::Nanoseconds;
frac_value
}
_ => {
time_unit = TimeUnit::Nanoseconds;
let truncated = parse_u32_fixed(bytes, frac_start, 9)?;
truncated
}
};
}
}
let mut tz_offset_seconds = 0i64;
if pos < len {
match bytes[pos] {
b'Z' | b'z' => {
pos += 1;
}
b'+' | b'-' => {
let tz_sign = if bytes[pos] == b'-' { -1 } else { 1 };
pos += 1;
if pos + 5 > len {
return Err(ParseError::InvalidTimezone);
}
let tz_hours = parse_u32_fixed(bytes, pos, 2)? as i64;
pos += 2;
if bytes.get(pos) != Some(&b':') {
return Err(ParseError::InvalidTimezone);
}
pos += 1;
let tz_minutes = parse_u32_fixed(bytes, pos, 2)? as i64;
pos += 2;
if tz_hours >= 24 || tz_minutes >= 60 {
return Err(ParseError::InvalidTimezone);
}
tz_offset_seconds = tz_sign * (tz_hours * 3600 + tz_minutes * 60);
}
_ => return Err(ParseError::InvalidFormat),
}
}
if pos != len {
return Err(ParseError::InvalidFormat);
}
let comp = DateTimeComponents {
year,
month,
day,
hour,
minute,
second,
nanosecond,
};
let mut timestamp = components_to_timestamp(&comp, time_unit);
if tz_offset_seconds != 0 {
timestamp -= tz_offset_seconds
* match time_unit {
TimeUnit::Seconds => 1,
TimeUnit::Milliseconds => 1_000,
TimeUnit::Microseconds => 1_000_000,
TimeUnit::Nanoseconds => 1_000_000_000,
TimeUnit::Days => unreachable!(),
};
}
Ok((timestamp, time_unit))
}
pub fn parse_rfc3339(s: &str) -> Result<(i64, TimeUnit), ParseError> {
parse_iso8601(s)
}
pub fn parse_datetime_flexible(s: &str) -> Result<(i64, TimeUnit), ParseError> {
if let Ok(result) = parse_iso8601(s) {
return Ok(result);
}
if let Ok(ms) = s.parse::<i64>() {
return Ok((ms, TimeUnit::Milliseconds));
}
Err(ParseError::InvalidFormat)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_date_only() {
let (ts, unit) = parse_iso8601("1970-01-01").unwrap();
assert_eq!(ts, 0);
assert_eq!(unit, TimeUnit::Seconds);
let (ts, unit) = parse_iso8601("2000-01-01").unwrap();
assert_eq!(ts, 946684800); assert_eq!(unit, TimeUnit::Seconds);
}
#[test]
fn test_parse_datetime() {
let (ts, unit) = parse_iso8601("1970-01-01T00:00:00").unwrap();
assert_eq!(ts, 0);
assert_eq!(unit, TimeUnit::Seconds);
let (ts, unit) = parse_iso8601("2000-01-01T12:34:56").unwrap();
assert_eq!(ts, 946730096);
assert_eq!(unit, TimeUnit::Seconds);
}
#[test]
fn test_parse_datetime_with_millis() {
let (ts, unit) = parse_iso8601("1970-01-01T00:00:00.123").unwrap();
assert_eq!(ts, 123);
assert_eq!(unit, TimeUnit::Milliseconds);
}
#[test]
fn test_parse_datetime_with_micros() {
let (ts, unit) = parse_iso8601("1970-01-01T00:00:00.123456").unwrap();
assert_eq!(ts, 123456);
assert_eq!(unit, TimeUnit::Microseconds);
}
#[test]
fn test_parse_datetime_with_nanos() {
let (ts, unit) = parse_iso8601("1970-01-01T00:00:00.123456789").unwrap();
assert_eq!(ts, 123456789);
assert_eq!(unit, TimeUnit::Nanoseconds);
}
#[test]
fn test_parse_datetime_utc() {
let (ts, unit) = parse_iso8601("1970-01-01T00:00:00Z").unwrap();
assert_eq!(ts, 0);
assert_eq!(unit, TimeUnit::Seconds);
}
#[test]
fn test_parse_datetime_with_timezone() {
let (ts, _) = parse_iso8601("1970-01-01T01:00:00+01:00").unwrap();
assert_eq!(ts, 0);
let (ts, _) = parse_iso8601("1970-01-01T00:00:00-05:00").unwrap();
assert_eq!(ts, 18000);
}
#[test]
fn test_parse_invalid_date() {
assert!(parse_iso8601("2000-13-01").is_err()); assert!(parse_iso8601("2000-02-30").is_err()); assert!(parse_iso8601("2001-02-29").is_err()); }
#[test]
fn test_parse_valid_leap_day() {
assert!(parse_iso8601("2000-02-29").is_ok()); assert!(parse_iso8601("2004-02-29").is_ok());
}
#[test]
fn test_parse_invalid_time() {
assert!(parse_iso8601("2000-01-01T24:00:00").is_err()); assert!(parse_iso8601("2000-01-01T12:60:00").is_err()); assert!(parse_iso8601("2000-01-01T12:34:60").is_err()); }
#[test]
fn test_parse_flexible() {
let (ts, _) = parse_datetime_flexible("2000-01-01T00:00:00").unwrap();
assert_eq!(ts, 946684800);
let (ts, unit) = parse_datetime_flexible("1700000000000").unwrap();
assert_eq!(ts, 1700000000000);
assert_eq!(unit, TimeUnit::Milliseconds);
}
}