use crate::{
ast::Value,
parser::{
error::Error,
utils::{IResult, LocatedSpan, ToRange},
},
};
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
use nom::{
branch::alt,
character::complete::{char, digit1, one_of},
combinator::{map, map_res, opt, recognize, value},
sequence::tuple,
};
pub(crate) fn date_time(i: LocatedSpan) -> IResult<Value> {
map(
tuple((date, char('T'), time, opt(timezone))),
|(date, _, time, timezone)| match timezone {
Some(tz) => Value::DateTime(DateTime::from_utc(NaiveDateTime::new(date, time) - tz, tz)),
None => Value::NaiveDateTime(NaiveDateTime::new(date, time)),
},
)(i)
}
pub(crate) fn date_value(i: LocatedSpan) -> IResult<Value> {
map(date, Value::Date)(i)
}
fn date(i: LocatedSpan) -> IResult<NaiveDate> {
map_res(
recognize(tuple((year, char('-'), month, char('-'), day))),
|res| {
NaiveDate::parse_from_str(*res, "%Y-%m-%d").map_err(|err| {
res
.extra
.report_error(Error(res.to_range(), "Invalid date".to_string()));
err
})
},
)(i)
}
fn time(i: LocatedSpan) -> IResult<NaiveTime> {
alt((
time_subsecond_precision,
time_second_precision,
time_minute_precision,
))(i)
}
fn time_subsecond_precision(i: LocatedSpan) -> IResult<NaiveTime> {
map_res(
recognize(tuple((
hour,
char(':'),
minute,
char(':'),
second,
char('.'),
digit1,
))),
|res| {
NaiveTime::parse_from_str(*res, "%H:%M:%S%.f").map_err(|err| {
res
.extra
.report_error(Error(res.to_range(), "Invalid time".to_string()));
err
})
},
)(i)
}
fn time_second_precision(i: LocatedSpan) -> IResult<NaiveTime> {
map_res(
recognize(tuple((hour, char(':'), minute, char(':'), second))),
|res| {
NaiveTime::parse_from_str(*res, "%H:%M:%S").map_err(|err| {
res
.extra
.report_error(Error(res.to_range(), "Invalid time".to_string()));
err
})
},
)(i)
}
fn time_minute_precision(i: LocatedSpan) -> IResult<NaiveTime> {
map_res(recognize(tuple((hour, char(':'), minute))), |res| {
NaiveTime::parse_from_str(*res, "%H:%M").map_err(|err| {
res
.extra
.report_error(Error(res.to_range(), "Invalid time".to_string()));
err
})
})(i)
}
fn year(i: LocatedSpan) -> IResult<LocatedSpan> {
recognize(tuple((dec_digit, dec_digit, dec_digit, dec_digit)))(i)
}
fn month(i: LocatedSpan) -> IResult<LocatedSpan> {
recognize(alt((
tuple((char('1'), one_of("012"))),
tuple((char('0'), dec_digit)),
)))(i)
}
fn day(i: LocatedSpan) -> IResult<LocatedSpan> {
recognize(alt((
tuple((char('3'), one_of("01"))),
tuple((one_of("012"), dec_digit)),
)))(i)
}
fn hour(i: LocatedSpan) -> IResult<LocatedSpan> {
recognize(alt((
tuple((char('2'), one_of("0123"))),
tuple((one_of("01"), dec_digit)),
)))(i)
}
fn minute(i: LocatedSpan) -> IResult<LocatedSpan> {
recognize(tuple((one_of("012345"), dec_digit)))(i)
}
fn second(i: LocatedSpan) -> IResult<LocatedSpan> {
recognize(alt((
tuple((char('6'), char('0'))),
tuple((one_of("012345"), dec_digit)),
)))(i)
}
fn dec_digit(i: LocatedSpan) -> IResult<char> {
one_of("0123456789")(i)
}
fn timezone(i: LocatedSpan) -> IResult<FixedOffset> {
alt((timezone_utc, timezone_offset))(i)
}
fn timezone_utc(i: LocatedSpan) -> IResult<FixedOffset> {
value(FixedOffset::east(0), char('Z'))(i)
}
fn timezone_offset(i: LocatedSpan) -> IResult<FixedOffset> {
map_res(
tuple((
one_of("+-"),
time_minute_precision,
)),
|(sign, offset)| match sign {
'+' => Ok(FixedOffset::east(offset.num_seconds_from_midnight() as i32)),
'-' => Ok(FixedOffset::west(offset.num_seconds_from_midnight() as i32)),
_ => Err("Invalid sign"),
},
)(i)
}
#[cfg(test)]
mod tests {
use crate::parser::{
absolute_time::date,
utils::{span, unwrap_span},
};
use chrono::NaiveDate;
#[test]
fn test_date() {
assert_eq!(
date(span("2020-01-01")).map(unwrap_span),
Ok(("", NaiveDate::from_ymd(2020, 01, 01)))
);
assert_eq!(
date(span("2024-02-29")).map(unwrap_span),
Ok(("", NaiveDate::from_ymd(2024, 02, 29)))
);
assert!(date(span("2023-02-29")).is_err());
}
}