use std::fmt::{Display, Formatter};
use crate::{
errors::{syntax_error, ParseResult},
Parser,
};
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum DateTime {
Date(Date),
Time(Time),
DateTime(Date, Time),
}
impl Display for DateTime {
fn fmt(&self, form: &mut Formatter) -> std::fmt::Result {
match self {
Self::Date(date) => write!(form, "{}", date),
Self::Time(time) => write!(form, "{}", time),
Self::DateTime(date, time) => write!(form, "{}T{}", date, time),
}
}
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum TimeOffset {
Local,
Offset {
plus: bool,
hours: u32,
minutes: u32,
},
}
impl Display for TimeOffset {
fn fmt(&self, form: &mut Formatter) -> std::fmt::Result {
if let Self::Offset {
plus,
hours,
minutes,
} = self
{
if *hours == 0 && *minutes == 0 {
write!(form, "Z")
} else if *plus {
write!(form, "+{:02}:{:02}", hours, minutes)
} else {
write!(form, "-{:02}:{:02}", hours, minutes)
}
} else {
write!(form, "")
}
}
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum SecondsFraction {
None,
Milliseconds(u32),
Microseconds(u32),
Nanoseconds(u32),
}
impl Display for SecondsFraction {
fn fmt(&self, form: &mut Formatter) -> std::fmt::Result {
match self {
Self::None => write!(form, ""),
Self::Milliseconds(millis) => write!(form, ".{:03}", millis),
Self::Microseconds(micros) => write!(form, ".{:06}", micros),
Self::Nanoseconds(nanos) => write!(form, ".{:09}", nanos),
}
}
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct Date {
pub year: u32,
pub month: u32,
pub day: u32,
}
impl Display for Date {
fn fmt(&self, form: &mut Formatter) -> std::fmt::Result {
write!(form, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
}
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct Time {
pub hour: u32,
pub minute: u32,
pub second: u32,
pub fraction: SecondsFraction,
pub offset: TimeOffset,
}
impl Display for Time {
fn fmt(&self, form: &mut Formatter) -> std::fmt::Result {
write!(
form,
"{:02}:{:02}:{:02}{}{}",
self.hour, self.minute, self.second, self.fraction, self.offset
)
}
}
fn parse_fixed_number(parser: &mut Parser, digits: usize) -> ParseResult<u32> {
let mut value = 0;
for _ in 0..digits {
if parser.is_at_eof() {
return Err(syntax_error(
parser.loc(),
"Expected a digit but reached end of stream.",
));
}
let ch = parser.peek();
if !ch.is_ascii_digit() {
return Err(syntax_error(
parser.loc(),
&format!("Expected a digit but found {:?} instead.", ch),
));
}
parser.consume();
value *= 10;
value += (ch as u32) - ('0' as u32);
}
Ok(value)
}
fn parse_date(parser: &mut Parser) -> ParseResult<Date> {
let year = parse_fixed_number(parser, 4)?;
parser.expect('-')?;
let month_loc = parser.loc();
let month = parse_fixed_number(parser, 2)?;
parser.expect('-')?;
let day_loc = parser.loc();
let day = parse_fixed_number(parser, 2)?;
if !(1..=12).contains(&month) {
return Err(syntax_error(
month_loc,
&format!(
"Month value {} is out of range; it must be in [1..12].",
month
),
));
}
if !(1..=31).contains(&day) {
return Err(syntax_error(
day_loc,
&format!("The day index {} is out of range for month {}.", day, month),
));
}
match month {
4 | 6 | 9 | 11 => {
if day > 30 {
return Err(syntax_error(
day_loc,
&format!("The day index {} is out of range for month {}.", day, month),
));
}
}
2 => {
if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
if day > 29 {
return Err(syntax_error(
day_loc,
&format!("The day index {} is out of range for month {}.", day, month),
));
}
} else {
if day > 28 {
return Err(syntax_error(
day_loc,
&format!("The day index {} is out of range for month {}.", day, month),
));
}
}
}
_ => {}
}
if ['+', '-'].contains(&parser.peek()) {
return Err(syntax_error(
parser.loc(),
&format!(
"Found unexpected character {:?} at end of date string.",
parser.peek()
),
));
}
if ![' ', 'T', 't'].contains(&parser.peek()) && parser.peek().is_ascii_alphanumeric() {
return Err(syntax_error(
parser.loc(),
&format!(
"Found unexpected character {:?} at end of date string.",
parser.peek()
),
));
}
Ok(Date { year, month, day })
}
fn parse_time(parser: &mut Parser) -> ParseResult<Time> {
let hour_loc = parser.loc();
let hour = parse_fixed_number(parser, 2)?;
if parser.peek() == '.' {
return Err(syntax_error(
parser.loc(),
"Fractional hours are not permitted.",
));
}
parser.expect(':')?;
let minute_loc = parser.loc();
let minute = parse_fixed_number(parser, 2)?;
if parser.peek() == '.' {
return Err(syntax_error(
parser.loc(),
"Fractional minutes are not permitted.",
));
}
let (second, fraction, second_loc) = if parser.peek_and_consume(':') {
let second_loc = parser.loc();
let second = parse_fixed_number(parser, 2)?;
let (mut fraction, mut fraction_digits) = if parser.peek_and_consume('.') {
let fraction_loc = parser.loc();
let digits = parser.take_while(|ch| ch.is_ascii_digit());
if digits.is_empty() {
return Err(syntax_error(
fraction_loc,
"Expected digits following decimal point (fractional seconds), but did not find any."
));
}
if digits.len() > 9 {
let chars: Vec<char> = digits.chars().collect();
let pivot = chars[9];
let digits = chars[0..9].iter().collect::<String>();
let mut value = digits.parse::<u32>().unwrap_or(0);
if ('5'..='9').contains(&pivot) {
value += 1;
}
(value, digits.len())
} else {
(digits.parse::<u32>().unwrap_or(0), digits.len())
}
} else {
(0, 0)
};
while fraction_digits % 3 != 0 {
fraction_digits += 1;
fraction *= 10;
}
let fraction = if fraction_digits == 0 {
SecondsFraction::None
} else if fraction_digits == 3 {
SecondsFraction::Milliseconds(fraction)
} else if fraction_digits == 6 {
SecondsFraction::Microseconds(fraction)
} else {
SecondsFraction::Nanoseconds(fraction)
};
(second, fraction, second_loc)
} else {
(0, SecondsFraction::None, parser.loc())
};
let offset = if parser.peek_and_consume('Z') || parser.peek_and_consume('z') {
TimeOffset::Offset {
plus: true,
hours: 0,
minutes: 0,
}
} else if parser.peek_and_consume('+') {
let hours = parse_fixed_number(parser, 2)?;
parser.expect(':')?;
let minutes = parse_fixed_number(parser, 2)?;
TimeOffset::Offset {
plus: true,
hours,
minutes,
}
} else if parser.peek_and_consume('-') {
let hours = parse_fixed_number(parser, 2)?;
parser.expect(':')?;
let minutes = parse_fixed_number(parser, 2)?;
TimeOffset::Offset {
plus: false,
hours,
minutes,
}
} else if parser.peek().is_alphanumeric() {
return Err(syntax_error(
parser.loc(),
&format!(
"Found unexpected character {:?} at end of time string.",
parser.peek()
),
));
} else {
TimeOffset::Local
};
if !(0..=23).contains(&hour) {
return Err(syntax_error(
hour_loc,
&format!(
"Hour value {:02} is out of the allowed range [00..23].",
hour
),
));
}
if !(0..=59).contains(&minute) {
return Err(syntax_error(
minute_loc,
&format!(
"Minute value {:02} is out of the allowed range [00..59].",
hour
),
));
}
if !(0..=60).contains(&second) {
return Err(syntax_error(
second_loc,
&format!(
"Second value {:02} is out of the allowed range [00..59].",
hour
),
));
}
Ok(Time {
hour,
minute,
second,
fraction,
offset,
})
}
pub fn is_date(parser: &mut Parser) -> bool {
let look = parser.peek_n_vec(10);
look.len() == 10
&& look[0].is_ascii_digit()
&& look[1].is_ascii_digit()
&& look[2].is_ascii_digit()
&& look[3].is_ascii_digit()
&& look[4] == '-'
&& look[5].is_ascii_digit()
&& look[6].is_ascii_digit()
&& look[7] == '-'
&& look[8].is_ascii_digit()
&& look[9].is_ascii_digit()
}
pub fn is_time(parser: &mut Parser) -> bool {
let look = parser.peek_n_vec(8);
look.len() >= 5
&& look[0].is_ascii_digit()
&& look[1].is_ascii_digit()
&& look[2] == ':'
&& look[3].is_ascii_digit()
&& look[4].is_ascii_digit()
}
pub fn parse_date_time(parser: &mut Parser) -> ParseResult<Option<DateTime>> {
if is_date(parser) {
let date = parse_date(parser)?;
if parser.peek_and_consume('T')
|| parser.peek_and_consume('t')
|| (parser.peek_and_consume(' ') && is_time(parser))
{
let time = parse_time(parser)?;
Ok(Some(DateTime::DateTime(date, time)))
} else {
Ok(Some(DateTime::Date(date)))
}
} else if is_time(parser) {
let time = parse_time(parser)?;
Ok(Some(DateTime::Time(time)))
} else {
Ok(None)
}
}
#[cfg(test)]
mod test {
use super::parse_date;
use crate::errors::ParseResult;
use crate::parse_from_string;
use crate::parsers::datetime::{
parse_date_time, parse_time, Date, DateTime, SecondsFraction, Time, TimeOffset,
};
#[test]
fn test_dates() -> ParseResult<()> {
let mut parser = parse_from_string("1999-01-01");
assert_eq!(
parse_date(&mut parser)?,
Date {
year: 1999,
month: 1,
day: 1
}
);
parser = parse_from_string("2024-12-31");
assert_eq!(
parse_date(&mut parser)?,
Date {
year: 2024,
month: 12,
day: 31
}
);
parser = parse_from_string("2233-03-22");
assert_eq!(
parse_date(&mut parser)?,
Date {
year: 2233,
month: 3,
day: 22
}
);
parser = parse_from_string("2016-01-06");
assert_eq!(
parse_date(&mut parser)?,
Date {
year: 2016,
month: 1,
day: 6
}
);
parser = parse_from_string("0001-12-24");
assert_eq!(
parse_date(&mut parser)?,
Date {
year: 1,
month: 12,
day: 24
}
);
parser = parse_from_string("2031-07-03");
assert_eq!(
parse_date(&mut parser)?,
Date {
year: 2031,
month: 7,
day: 3
}
);
parser = parse_from_string("2032-07-04");
assert_eq!(
parse_date(&mut parser)?,
Date {
year: 2032,
month: 7,
day: 4
}
);
parser = parse_from_string("2004-02-29");
assert_eq!(
parse_date(&mut parser)?,
Date {
year: 2004,
month: 2,
day: 29
}
);
parser = parse_from_string("9999-12-31");
assert_eq!(
parse_date(&mut parser)?,
Date {
year: 9999,
month: 12,
day: 31
}
);
parser = parse_from_string("1900-02-29");
assert!(parse_date(&mut parser).is_err());
parser = parse_from_string("2001-04-31");
assert!(parse_date(&mut parser).is_err());
Ok(())
}
#[test]
fn test_times() -> ParseResult<()> {
let mut parser = parse_from_string("06:17:22");
assert_eq!(
parse_time(&mut parser)?,
Time {
hour: 6,
minute: 17,
second: 22,
fraction: SecondsFraction::None,
offset: TimeOffset::Local
}
);
parser = parse_from_string("12:59:59.1Z");
assert_eq!(
parse_time(&mut parser)?,
Time {
hour: 12,
minute: 59,
second: 59,
fraction: SecondsFraction::Milliseconds(100),
offset: TimeOffset::Offset {
plus: true,
hours: 0,
minutes: 0
}
}
);
parser = parse_from_string("12:59:20.000126+02:00");
assert_eq!(
parse_time(&mut parser)?,
Time {
hour: 12,
minute: 59,
second: 20,
fraction: SecondsFraction::Microseconds(126),
offset: TimeOffset::Offset {
plus: true,
hours: 2,
minutes: 0
}
}
);
parser = parse_from_string("23:57:00.000126000-04:00");
assert_eq!(
parse_time(&mut parser)?,
Time {
hour: 23,
minute: 57,
second: 00,
fraction: SecondsFraction::Nanoseconds(126000),
offset: TimeOffset::Offset {
plus: false,
hours: 4,
minutes: 0
}
}
);
Ok(())
}
#[test]
fn test_date_time() -> ParseResult<()> {
let mut parser = parse_from_string("1999-01-01T16:45:20Z");
assert_eq!(
parse_date_time(&mut parser)?,
Some(DateTime::DateTime(
Date {
year: 1999,
month: 1,
day: 1
},
Time {
hour: 16,
minute: 45,
second: 20,
fraction: SecondsFraction::None,
offset: TimeOffset::Offset {
plus: true,
hours: 0,
minutes: 0
}
}
))
);
parser = parse_from_string("2024-12-31 12:59:59.100");
assert_eq!(
parse_date_time(&mut parser)?,
Some(DateTime::DateTime(
Date {
year: 2024,
month: 12,
day: 31
},
Time {
hour: 12,
minute: 59,
second: 59,
fraction: SecondsFraction::Milliseconds(100),
offset: TimeOffset::Local
}
))
);
Ok(())
}
#[test]
fn test_circular() -> ParseResult<()> {
let tests = [
("1972-06-30T23:59:60Z", "1972-06-30T23:59:60Z"),
("2019-03-26T14:00:00.9Z", "2019-03-26T14:00:00.900Z"),
("2019-03-26T14:00:00.4999Z", "2019-03-26T14:00:00.499900Z"),
("1969-03-26T14:00:00.4999Z", "1969-03-26T14:00:00.499900Z"),
("2019-03-26t14:00Z", "2019-03-26T14:00:00Z"),
("2019-03-26T14:00z", "2019-03-26T14:00:00Z"),
("2019-03-26T14:00:00,999Z", "2019-03-26T14:00:00"),
("2019-03-26 14:00Z", "2019-03-26T14:00:00Z"),
("2019-03-26T10:00-04", ""),
("2019-03-26T14:00.9Z", ""),
("20190326T1400Z", "*"), ("2019-02-30", ""),
("2019-03-25T24:01Z", ""),
("2019-03-26Z", ""),
("2019-03-26+01:00", ""),
("2019-03-26-04:00", ""),
("2019-03-26T10:00-0400", ""),
("002019-03-26T14:00Z", "*"), ("019-03-26T14:00Z", "*"), ("2019-03-26T10:00Q", ""),
("2019-03-26T10:00T", ""),
("2019-03-26Q", ""),
("2019-03-26T", ""),
("2019-03-26T14:00:00.", ""),
("2019-03-26T14:00+24:00", "2019-03-26T14:00:00+24:00"),
("2018-06-30T23:59:60Z", "2018-06-30T23:59:60Z"),
("2019-03-26T23:59:60Z", "2019-03-26T23:59:60Z"),
("2019-03-26T13:59:60Z", "2019-03-26T13:59:60Z"),
("1922-09-12", "1922-09-12"),
("1924-03-24", "1924-03-24"),
("03:00", "03:00:00"),
("03:30:00", "03:30:00"),
("03:30:33.3", "03:30:33.300"),
("03:30:33.3033", "03:30:33.303300"),
("03:30:33.3300333", "03:30:33.330033300"),
("03:30:33.3300333336", "03:30:33.330033334"), ("03:30:33.3300333335", "03:30:33.330033334"),
("03:30:33.3300333334", "03:30:33.330033333"),
("03:30:33+02:00", "03:30:33+02:00"),
("03:30:33-02:30", "03:30:33-02:30"),
("1900-02-29", ""),
("2000-02-30", ""),
("2000-02-29", "2000-02-29"),
("1968-04-31", ""),
("1200-12-99", ""),
("1200-99-31", ""),
("12:31:5Z", ""),
("12:31:61", ""),
("12:60:59", ""),
("24:59:59", ""),
("1", "X"),
("1900-", "X"),
("T13:02", "X"),
];
for (input, output) in tests {
print!("Trying string {:?}... ", input);
let mut parser = parse_from_string(input);
if output.is_empty() {
assert!(parse_date_time(&mut parser).is_err());
} else if output == "X" {
assert_eq!(parse_date_time(&mut parser).unwrap(), None);
} else {
let result = parse_date_time(&mut parser)?;
match result {
None => assert!(output == "*"),
Some(dt) => assert_eq!(dt.to_string(), output),
}
}
println!("Done");
}
Ok(())
}
#[test]
fn forced_errors_test() {
let mut parser = parse_from_string("1X99-01-01");
assert!(parse_date(&mut parser).is_err());
let mut parser = parse_from_string("1299-X1-01");
assert!(parse_date(&mut parser).is_err());
let mut parser = parse_from_string("1299-01-X1");
assert!(parse_date(&mut parser).is_err());
let mut parser = parse_from_string("03.5:01:01");
assert!(parse_time(&mut parser).is_err());
let mut parser = parse_from_string("03:01.5:01");
assert!(parse_time(&mut parser).is_err());
let mut parser = parse_from_string("199912-23");
assert!(parse_date(&mut parser).is_err());
let mut parser = parse_from_string("1999-1223");
assert!(parse_date(&mut parser).is_err());
let mut parser = parse_from_string("03:21:45+0100");
assert!(parse_time(&mut parser).is_err());
}
}