start parsing date with winnow

This commit is contained in:
Terts Diepraam
2024-01-26 17:17:36 +01:00
committed by yuankunzhang
parent b481a2bdec
commit 07d4b80ea5
12 changed files with 1342 additions and 341 deletions
Generated
+10
View File
@@ -149,6 +149,7 @@ dependencies = [
"chrono",
"nom",
"regex",
"winnow",
]
[[package]]
@@ -348,3 +349,12 @@ name = "windows_x86_64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]]
name = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
+1
View File
@@ -11,3 +11,4 @@ readme = "README.md"
regex = "1.10.4"
chrono = { version="0.4.38", default-features=false, features=["std", "alloc", "clock"] }
nom = "8.0.0"
winnow = "0.5.34"
+76
View File
@@ -0,0 +1,76 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Parse an ISO 8601 date and time item
//!
//! The GNU docs state:
//!
//! > The ISO 8601 date and time of day extended format consists of an ISO 8601
//! > date, a T character separator, and an ISO 8601 time of day. This format
//! > is also recognized if the T is replaced by a space.
//! >
//! > In this format, the time of day should use 24-hour notation. Fractional
//! > seconds are allowed, with either comma or period preceding the fraction.
//! > ISO 8601 fractional minutes and hours are not supported. Typically, hosts
//! > support nanosecond timestamp resolution; excess precision is silently discarded.
use winnow::{combinator::alt, seq, PResult, Parser};
use crate::items::space;
use super::{
date::{self, Date},
s,
time::{self, Time},
};
#[derive(PartialEq, Debug, Clone)]
pub struct DateTime {
date: Date,
time: Time,
}
pub fn parse(input: &mut &str) -> PResult<DateTime> {
seq!(DateTime {
date: date::iso,
// Note: the `T` is lowercased by the main parse function
_: alt((s('t').void(), (' ', space).void())),
time: time::iso,
})
.parse_next(input)
}
#[cfg(test)]
mod tests {
use super::{parse, DateTime};
use crate::items::{date::Date, time::Time};
#[test]
fn some_date() {
let reference = Some(DateTime {
date: Date {
day: 10,
month: 10,
year: Some(2022),
},
time: Time {
hour: 10,
minute: 10,
second: 55.0,
offset: None,
},
});
for mut s in [
"2022-10-10t10:10:55",
"2022-10-10 10:10:55",
"2022-10-10 t 10:10:55",
"2022-10-10 10:10:55",
"2022-10-10 (A comment!) t 10:10:55",
"2022-10-10 (A comment!) 10:10:55",
] {
let old_s = s.to_owned();
assert_eq!(parse(&mut s).ok(), reference, "Failed string: {old_s}")
}
}
}
+232
View File
@@ -0,0 +1,232 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Parse a date item (without time component)
//!
//! The GNU docs say:
//!
//! > A calendar date item specifies a day of the year. It is specified
//! > differently, depending on whether the month is specified numerically
//! > or literally.
//! >
//! > ...
//! >
//! > For numeric months, the ISO 8601 format year-month-day is allowed,
//! > where year is any positive number, month is a number between 01 and
//! > 12, and day is a number between 01 and 31. A leading zero must be
//! > present if a number is less than ten. If year is 68 or smaller, then
//! > 2000 is added to it; otherwise, if year is less than 100, then 1900
//! > is added to it. The construct month/day/year, popular in the United
//! > States, is accepted. Also month/day, omitting the year.
//! >
//! > Literal months may be spelled out in full: January, February,
//! > March, April, May, June, July, August, September,
//! > October, November or December. Literal months may be
//! > abbreviated to their first three letters, possibly followed by an
//! > abbreviating dot. It is also permitted to write Sept instead of
//! > September.
use winnow::{
ascii::{alpha1, dec_uint},
combinator::{alt, opt, preceded},
seq,
token::take,
PResult, Parser,
};
use super::s;
use crate::ParseDateTimeError;
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Date {
pub day: u32,
pub month: u32,
pub year: Option<u32>,
}
pub fn parse(input: &mut &str) -> PResult<Date> {
alt((iso, us, literal1, literal2)).parse_next(input)
}
/// Parse `YYYY-MM-DD` or `YY-MM-DD`
///
/// This is also used by [`combined`](super::combined).
pub fn iso(input: &mut &str) -> PResult<Date> {
seq!(Date {
year: year.map(Some),
_: s('-'),
month: month,
_: s('-'),
day: day,
})
.parse_next(input)
}
/// Parse `MM/DD/YYYY`, `MM/DD/YY` or `MM/DD`
fn us(input: &mut &str) -> PResult<Date> {
seq!(Date {
month: month,
_: s('/'),
day: day,
year: opt(preceded(s('/'), year)),
})
.parse_next(input)
}
/// Parse `14 November 2022`, `14 Nov 2022`, "14nov2022", "14-nov-2022", "14-nov2022", "14nov-2022"
fn literal1(input: &mut &str) -> PResult<Date> {
seq!(Date {
day: day,
_: opt(s('-')),
month: literal_month,
year: opt(preceded(opt(s('-')), year)),
})
.parse_next(input)
}
/// Parse `November 14, 2022` and `Nov 14, 2022`
fn literal2(input: &mut &str) -> PResult<Date> {
seq!(Date {
month: literal_month,
day: day,
// FIXME: GNU requires _some_ space between the day and the year,
// probably to distinguish with floats.
year: opt(preceded(s(","), year)),
})
.parse_next(input)
}
fn year(input: &mut &str) -> PResult<u32> {
s(alt((
take(4usize).try_map(|x: &str| x.parse()),
take(3usize).try_map(|x: &str| x.parse()),
take(2usize).try_map(|x: &str| x.parse()).map(
|x: u32| {
if x <= 68 {
x + 2000
} else {
x + 1900
}
},
),
)))
.parse_next(input)
}
fn month(input: &mut &str) -> PResult<u32> {
s(dec_uint)
.try_map(|x| {
(x >= 1 && x <= 12)
.then_some(x)
.ok_or(ParseDateTimeError::InvalidInput)
})
.parse_next(input)
}
fn day(input: &mut &str) -> PResult<u32> {
s(dec_uint)
.try_map(|x| {
(x >= 1 && x <= 31)
.then_some(x)
.ok_or(ParseDateTimeError::InvalidInput)
})
.parse_next(input)
}
/// Parse the name of a month (case-insensitive)
fn literal_month(input: &mut &str) -> PResult<u32> {
s(alpha1)
.verify_map(|s: &str| {
Some(match s {
"january" | "jan" => 1,
"february" | "feb" => 2,
"march" | "mar" => 3,
"april" | "apr" => 4,
"may" => 5,
"june" | "jun" => 6,
"july" | "jul" => 7,
"august" | "aug" => 8,
"september" | "sep" | "sept" => 9,
"october" | "oct" => 10,
"november" | "nov" => 11,
"december" | "dec" => 12,
_ => return None,
})
})
.parse_next(input)
}
#[cfg(test)]
mod tests {
use super::{parse, Date};
// Test cases from the GNU docs:
//
// ```
// 2022-11-14 # ISO 8601.
// 22-11-14 # Assume 19xx for 69 through 99,
// # 20xx for 00 through 68 (not recommended).
// 11/14/2022 # Common U.S. writing.
// 14 November 2022
// 14 Nov 2022 # Three-letter abbreviations always allowed.
// November 14, 2022
// 14-nov-2022
// 14nov2022
// ```
#[test]
fn with_year() {
let reference = Date {
year: Some(2022),
month: 11,
day: 14,
};
for mut s in [
"2022-11-14",
"2022 - 11 - 14",
"22-11-14",
"2022---11----14",
"22(comment 1)-11(comment 2)-14",
"11/14/2022",
"11--/14--/2022",
"11(comment 1)/(comment 2)14(comment 3)/(comment 4)2022",
"11 / 14 / 2022",
"11/14/22",
"14 november 2022",
"14 nov 2022",
"november 14, 2022",
"november 14 , 2022",
"nov 14, 2022",
"14-nov-2022",
"14nov2022",
"14nov 2022",
] {
let old_s = s.to_owned();
assert_eq!(parse(&mut s).unwrap(), reference, "Format string: {old_s}");
}
}
#[test]
fn no_year() {
let reference = Date {
year: None,
month: 11,
day: 14,
};
for mut s in [
"11/14",
"14 november",
"14 nov",
"14(comment!)nov",
"november 14",
"november(comment!)14",
"nov 14",
"14-nov",
"14nov",
"14(comment????)nov",
] {
assert_eq!(parse(&mut s).unwrap(), reference);
}
}
}
+158
View File
@@ -0,0 +1,158 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore multispace0
//! From the GNU docs:
//!
//! > A date is a string, possibly empty, containing many items separated by
//! > whitespace. The whitespace may be omitted when no ambiguity arises. The
//! > empty string means the beginning of today (i.e., midnight). Order of the
//! > items is immaterial. A date string may contain many flavors of items:
//! > - calendar date items
//! > - time of day items
//! > - time zone items
//! > - combined date and time of day items
//! > - day of the week items
//! > - relative items
//! > - pure numbers.
//!
//! We put all of those in separate modules:
//! - [`date`]
//! - [`time`]
//! - [`time_zone`]
//! - [`combined`]
//! - [`weekday`]
//! - [`relative`]
//! - [`number]
mod combined;
mod date;
mod ordinal;
mod relative;
mod time;
mod time_zone;
mod weekday;
mod number {}
use winnow::{
ascii::multispace0,
combinator::{alt, delimited, not, peek, preceded, repeat, separated, terminated},
error::ParserError,
stream::AsChar,
token::{none_of, take_while},
PResult, Parser,
};
#[derive(PartialEq, Debug)]
pub enum Item {
DateTime(combined::DateTime),
Date(date::Date),
Time(time::Time),
Weekday(weekday::Weekday),
Relative(relative::Relative),
TimeZone(()),
}
/// Allow spaces and comments before a parser
///
/// Every token parser should be wrapped in this to allow spaces and comments.
/// It is only preceding, because that allows us to check mandatory whitespace
/// after running the parser.
fn s<'a, O, E>(p: impl Parser<&'a str, O, E>) -> impl Parser<&'a str, O, E>
where
E: ParserError<&'a str>,
{
preceded(space, p)
}
/// Parse the space in-between tokens
///
/// You probably want to use the [`s`] combinator instead.
fn space<'a, E>(input: &mut &'a str) -> PResult<(), E>
where
E: ParserError<&'a str>,
{
separated(0.., multispace0, alt((comment, ignored_hyphen_or_plus))).parse_next(input)
}
/// A hyphen or plus is ignored when it is not followed by a digit
///
/// This includes being followed by a comment! Compare these inputs:
/// ```txt
/// - 12 weeks
/// - (comment) 12 weeks
/// ```
/// The last comment should be ignored.
///
/// The plus is undocumented, but it seems to be ignored.
fn ignored_hyphen_or_plus<'a, E>(input: &mut &'a str) -> PResult<(), E>
where
E: ParserError<&'a str>,
{
(
alt(('-', '+')),
multispace0,
peek(not(take_while(1, AsChar::is_dec_digit))),
)
.void()
.parse_next(input)
}
/// Parse a comment
///
/// A comment is given between parentheses, which must be balanced. Any other
/// tokens can be within the comment.
fn comment<'a, E>(input: &mut &'a str) -> PResult<(), E>
where
E: ParserError<&'a str>,
{
delimited(
'(',
repeat(0.., alt((none_of(['(', ')']).void(), comment))),
')',
)
.parse_next(input)
}
/// Parse an item
pub fn parse_one(input: &mut &str) -> PResult<Item> {
alt((
combined::parse.map(Item::DateTime),
date::parse.map(Item::Date),
time::parse.map(Item::Time),
relative::parse.map(Item::Relative),
weekday::parse.map(Item::Weekday),
// time_zone::parse.map(Item::TimeZone),
))
.parse_next(input)
}
pub fn parse(input: &mut &str) -> Option<Vec<Item>> {
terminated(repeat(0.., parse_one), space).parse(input).ok()
}
#[cfg(test)]
mod tests {
use super::{date::Date, parse, time::Time, Item};
#[test]
fn date_and_time() {
assert_eq!(
parse(&mut " 10:10 2022-12-12 "),
Some(vec![
Item::Time(Time {
hour: 10,
minute: 10,
second: 0.0,
offset: None,
}),
Item::Date(Date {
day: 12,
month: 12,
year: Some(2022)
})
])
)
}
}
+45
View File
@@ -0,0 +1,45 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Numbers without other symbols
//!
//! The GNU docs state:
//!
//! > If the decimal number is of the form yyyymmdd and no other calendar date
//! > item (see Calendar date items) appears before it in the date string, then
//! > yyyy is read as the year, mm as the month number and dd as the day of the
//! > month, for the specified calendar date.
//! >
//! > If the decimal number is of the form hhmm and no other time of day item
//! > appears before it in the date string, then hh is read as the hour of the
//! > day and mm as the minute of the hour, for the specified time of day. mm
//! > can also be omitted.
use winnow::{combinator::cond, PResult};
enum Number {
Date,
Time,
Year,
}
pub fn parse(seen_date: bool, seen_time: bool, input: &mut &str) -> PResult<Number> {
alt((
cond(!seen_date, date_number),
cond(!seen_time, time_number),
cond(seen_date && seen_time, year_number),
))
.parse_next(input)
}
fn date_number(input: &mut &str) -> PResult<Number> {
todo!()
}
fn time_number(input: &mut &str) -> PResult<Number> {
todo!()
}
fn year_number(input: &mut &str) -> PResult<Number> {
todo!()
}
+46
View File
@@ -0,0 +1,46 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use super::s;
use winnow::{
ascii::{alpha1, dec_uint},
combinator::{alt, opt},
PResult, Parser,
};
pub fn ordinal(input: &mut &str) -> PResult<i32> {
alt((text_ordinal, number_ordinal)).parse_next(input)
}
fn number_ordinal(input: &mut &str) -> PResult<i32> {
let sign = opt(alt(('+'.value(1), '-'.value(-1)))).map(|s| s.unwrap_or(1));
(s(sign), s(dec_uint))
.verify_map(|(s, u): (i32, u32)| {
let i: i32 = u.try_into().ok()?;
Some(s * i)
})
.parse_next(input)
}
fn text_ordinal(input: &mut &str) -> PResult<i32> {
s(alpha1)
.verify_map(|s: &str| {
Some(match s {
"last" => -1,
"this" => 0,
"next" | "first" => 1,
"third" => 3,
"fourth" => 4,
"fifth" => 5,
"sixth" => 6,
"seventh" => 7,
"eight" => 8,
"ninth" => 9,
"tenth" => 10,
"eleventh" => 11,
"twelfth" => 12,
_ => return None,
})
})
.parse_next(input)
}
+190
View File
@@ -0,0 +1,190 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Parse a relative datetime item
//!
//! The GNU docs state:
//!
//! > The unit of time displacement may be selected by the string year or
//! > month for moving by whole years or months. These are fuzzy units, as
//! > years and months are not all of equal duration. More precise units are
//! > fortnight which is worth 14 days, week worth 7 days, day worth 24
//! > hours, hour worth 60 minutes, minute or min worth 60 seconds, and
//! > second or sec worth one second. An s suffix on these units is
//! > accepted and ignored.
//! >
//! > The unit of time may be preceded by a multiplier, given as an optionally
//! > signed number. Unsigned numbers are taken as positively signed. No number
//! > at all implies 1 for a multiplier. Following a relative item by the
//! > string ago is equivalent to preceding the unit by a multiplier with
//! > value -1.
//! >
//! > The string tomorrow is worth one day in the future (equivalent to
//! > day), the string yesterday is worth one day in the past (equivalent
//! > to day ago).
//! >
//! > The strings now or today are relative items corresponding to
//! > zero-valued time displacement, these strings come from the fact a
//! > zero-valued time displacement represents the current time when not
//! > otherwise changed by previous items. They may be used to stress other
//! > items, like in 12:00 today. The string this also has the meaning of a
//! > zero-valued time displacement, but is preferred in date strings like
//! > this thursday.
use winnow::{
ascii::{alpha1, float},
combinator::{alt, opt},
PResult, Parser,
};
use super::{ordinal::ordinal, s};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Relative {
Years(i32),
Months(i32),
Days(i32),
Hours(i32),
Minutes(i32),
// Seconds are special because they can be given as a float
Seconds(f64),
}
impl Relative {
fn mul(self, n: i32) -> Self {
match self {
Self::Years(x) => Self::Years(n * x),
Self::Months(x) => Self::Months(n * x),
Self::Days(x) => Self::Days(n * x),
Self::Hours(x) => Self::Hours(n * x),
Self::Minutes(x) => Self::Minutes(n * x),
Self::Seconds(x) => Self::Seconds(f64::from(n) * x),
}
}
}
pub fn parse(input: &mut &str) -> PResult<Relative> {
alt((
s("tomorrow").value(Relative::Days(1)),
s("yesterday").value(Relative::Days(-1)),
// For "today" and "now", the unit is arbitrary
s("today").value(Relative::Days(0)),
s("now").value(Relative::Days(0)),
seconds,
other,
))
.parse_next(input)
}
fn seconds(input: &mut &str) -> PResult<Relative> {
(
opt(alt((s(float), ordinal.map(|x| x as f64)))),
s(alpha1).verify(|s: &str| matches!(s, "seconds" | "second" | "sec" | "secs")),
ago,
)
.map(|(n, _, ago)| Relative::Seconds(n.unwrap_or(1.0) * if ago { -1.0 } else { 1.0 }))
.parse_next(input)
}
fn other(input: &mut &str) -> PResult<Relative> {
(opt(ordinal), integer_unit, ago)
.map(|(n, unit, ago)| unit.mul(n.unwrap_or(1) * if ago { -1 } else { 1 }))
.parse_next(input)
}
fn ago(input: &mut &str) -> PResult<bool> {
opt(s("ago")).map(|o| o.is_some()).parse_next(input)
}
fn integer_unit(input: &mut &str) -> PResult<Relative> {
s(alpha1)
.verify_map(|s: &str| {
Some(match s.strip_suffix('s').unwrap_or(&s) {
"year" => Relative::Years(1),
"month" => Relative::Months(1),
"fortnight" => Relative::Days(14),
"week" => Relative::Days(7),
"day" => Relative::Days(1),
"hour" => Relative::Hours(1),
"minute" | "min" => Relative::Minutes(1),
_ => return None,
})
})
.parse_next(input)
}
#[cfg(test)]
mod tests {
use super::{parse, Relative};
#[test]
fn all() {
for (s, rel) in [
// Seconds
("second", Relative::Seconds(1.0)),
("sec", Relative::Seconds(1.0)),
("seconds", Relative::Seconds(1.0)),
("secs", Relative::Seconds(1.0)),
("second ago", Relative::Seconds(-1.0)),
("3 seconds", Relative::Seconds(3.0)),
("3.5 seconds", Relative::Seconds(3.5)),
// ("+3.5 seconds", Relative::Seconds(3.5)),
("3.5 seconds ago", Relative::Seconds(-3.5)),
("-3.5 seconds ago", Relative::Seconds(3.5)),
// Minutes
("minute", Relative::Minutes(1)),
("minutes", Relative::Minutes(1)),
("min", Relative::Minutes(1)),
("mins", Relative::Minutes(1)),
("10 minutes", Relative::Minutes(10)),
("-10 minutes", Relative::Minutes(-10)),
("10 minutes ago", Relative::Minutes(-10)),
("-10 minutes ago", Relative::Minutes(10)),
// Hours
("hour", Relative::Hours(1)),
("hours", Relative::Hours(1)),
("10 hours", Relative::Hours(10)),
("+10 hours", Relative::Hours(10)),
("-10 hours", Relative::Hours(-10)),
("10 hours ago", Relative::Hours(-10)),
("-10 hours ago", Relative::Hours(10)),
// Days
("day", Relative::Days(1)),
("days", Relative::Days(1)),
("10 days", Relative::Days(10)),
("+10 days", Relative::Days(10)),
("-10 days", Relative::Days(-10)),
("10 days ago", Relative::Days(-10)),
("-10 days ago", Relative::Days(10)),
// Multiple days
("fortnight", Relative::Days(14)),
("fortnights", Relative::Days(14)),
("2 fortnights ago", Relative::Days(-28)),
("+2 fortnights ago", Relative::Days(-28)),
("week", Relative::Days(7)),
("weeks", Relative::Days(7)),
("2 weeks ago", Relative::Days(-14)),
// Other
("year", Relative::Years(1)),
("years", Relative::Years(1)),
("month", Relative::Months(1)),
("months", Relative::Months(1)),
// Special
("yesterday", Relative::Days(-1)),
("tomorrow", Relative::Days(1)),
("today", Relative::Days(0)),
("now", Relative::Days(0)),
// This something
("this day", Relative::Days(0)),
("this second", Relative::Seconds(0.0)),
("this year", Relative::Years(0)),
// Weird stuff
("next week ago", Relative::Days(-7)),
("last week ago", Relative::Days(7)),
("this week ago", Relative::Days(0)),
] {
let mut t = s;
assert_eq!(parse(&mut t).ok(), Some(rel), "Failed string: {s}")
}
}
}
+438
View File
@@ -0,0 +1,438 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore shhmm colonless
//! Parse a time item (without a date)
//!
//! The GNU docs state:
//!
//! > More generally, the time of day may be given as hour:minute:second,
//! > where hour is a number between 0 and 23, minute is a number between 0 and
//! > 59, and second is a number between 0 and 59 possibly followed by . or
//! > , and a fraction containing one or more digits. Alternatively,
//! > :second can be omitted, in which case it is taken to be zero. On the
//! > rare hosts that support leap seconds, second may be 60.
//! >
//! > If the time is followed by am or pm (or a.m. or p.m.), hour is
//! > restricted to run from 1 to 12, and :minute may be omitted (taken to be
//! > zero). am indicates the first half of the day, pm indicates the
//! > second half of the day. In this notation, 12 is the predecessor of 1:
//! > midnight is 12am while noon is 12pm. (This is the zero-oriented
//! > interpretation of 12am and 12pm, as opposed to the old tradition
//! > derived from Latin which uses 12m for noon and 12pm for midnight.)
//! >
//! > The time may alternatively be followed by a time zone correction,
//! > expressed as shhmm, where s is + or -, hh is a number of zone hours
//! > and mm is a number of zone minutes. The zone minutes term, mm, may be
//! > omitted, in which case the one- or two-digit correction is interpreted as
//! > a number of hours. You can also separate hh from mm with a colon. When a
//! > time zone correction is given this way, it forces interpretation of the
//! > time relative to Coordinated Universal Time (UTC), overriding any
//! > previous specification for the time zone or the local time zone. For
//! > example, +0530 and +05:30 both stand for the time zone 5.5 hours
//! > ahead of UTC (e.g., India). This is the best way to specify a time zone
//! > correction by fractional parts of an hour. The maximum zone correction is
//! > 24 hours.
//! >
//! > Either am/pm or a time zone correction may be specified, but not both.
use winnow::{
ascii::{dec_uint, float},
combinator::{alt, opt, preceded},
seq,
stream::AsChar,
token::take_while,
PResult, Parser,
};
use super::s;
#[derive(PartialEq, Clone, Debug)]
pub struct Time {
pub hour: u32,
pub minute: u32,
pub second: f64,
pub offset: Option<Offset>,
}
#[derive(PartialEq, Debug, Clone)]
pub struct Offset {
negative: bool,
hours: u32,
minutes: u32,
}
#[derive(Clone)]
enum Suffix {
Am,
Pm,
}
pub fn parse(input: &mut &str) -> PResult<Time> {
alt((am_pm_time, iso)).parse_next(input)
}
/// Parse an ISO 8601 time string
///
/// Also used by the [`combined`](super::combined) module
pub fn iso(input: &mut &str) -> PResult<Time> {
alt((
(hour24, timezone).map(|(hour, offset)| Time {
hour,
minute: 0,
second: 0.0,
offset: Some(offset),
}),
seq!( Time {
hour: hour24,
_: colon,
minute: minute,
second: opt(preceded(colon, second)).map(|s| s.unwrap_or(0.0)),
offset: opt(timezone)
}),
))
.parse_next(input)
}
/// Parse a time ending with AM or PM
///
/// The hours are restricted to 12 or lower in this format
fn am_pm_time(input: &mut &str) -> PResult<Time> {
seq!(
hour12,
opt(preceded(colon, minute)),
opt(preceded(colon, second)),
alt((
s("am").value(Suffix::Am),
s("a.m.").value(Suffix::Am),
s("pm").value(Suffix::Pm),
s("p.m.").value(Suffix::Pm)
)),
)
.map(|(h, m, s, suffix)| {
let mut h = h % 12;
if let Suffix::Pm = suffix {
h += 12;
}
Time {
hour: h,
minute: m.unwrap_or(0),
second: s.unwrap_or(0.0),
offset: None,
}
})
.parse_next(input)
}
/// Parse a colon preceded by whitespace
fn colon(input: &mut &str) -> PResult<()> {
s(':').void().parse_next(input)
}
/// Parse a number of hours in `0..24`(preceded by whitespace)
fn hour24(input: &mut &str) -> PResult<u32> {
s(dec_uint).verify(|x| *x < 24).parse_next(input)
}
/// Parse a number of hours in `0..=12` (preceded by whitespace)
fn hour12(input: &mut &str) -> PResult<u32> {
s(dec_uint).verify(|x| *x <= 12).parse_next(input)
}
/// Parse a number of minutes (preceded by whitespace)
fn minute(input: &mut &str) -> PResult<u32> {
s(dec_uint).verify(|x| *x < 60).parse_next(input)
}
/// Parse a number of seconds (preceded by whitespace)
fn second(input: &mut &str) -> PResult<f64> {
s(float).verify(|x| *x < 60.0).parse_next(input)
}
/// Parse a timezone starting with `+` or `-`
fn timezone(input: &mut &str) -> PResult<Offset> {
seq!(plus_or_minus, alt((timezone_colon, timezone_colonless)))
.map(|(negative, (hours, minutes))| Offset {
negative,
hours,
minutes,
})
.parse_next(input)
}
/// Parse a timezone offset with a colon separating hours and minutes
fn timezone_colon(input: &mut &str) -> PResult<(u32, u32)> {
seq!(
s(take_while(1..=2, AsChar::is_dec_digit)).try_map(|x: &str| {
// parse will fail on empty input
if x == "" {
Ok(0)
} else {
x.parse()
}
}),
_: colon,
s(take_while(1..=2, AsChar::is_dec_digit)).try_map(|x: &str| x.parse()),
)
.parse_next(input)
}
/// Parse a timezone offset without colon
fn timezone_colonless(input: &mut &str) -> PResult<(u32, u32)> {
s(take_while(0..=4, AsChar::is_dec_digit))
.verify_map(|x: &str| {
Some(match x.len() {
0 => (0, 0),
1 | 2 => (x.parse().ok()?, 0),
// The minutes are the last two characters here, for some reason.
3 => (x[..1].parse().ok()?, x[1..].parse().ok()?),
4 => (x[..2].parse().ok()?, x[2..].parse().ok()?),
_ => unreachable!("We only take up to 4 characters"),
})
})
.parse_next(input)
}
/// Parse the plus or minus character and return whether it was negative
fn plus_or_minus(input: &mut &str) -> PResult<bool> {
s(alt(("+".value(false), "-".value(true)))).parse_next(input)
}
#[cfg(test)]
mod tests {
use super::{Offset, Time};
use crate::items::time::parse;
#[test]
fn simple() {
let reference = Time {
hour: 20,
minute: 2,
second: 0.0,
offset: None,
};
for mut s in [
"20:02:00.000000",
"20:02:00",
"20:02+:00",
"20:02-:00",
"20----:02--(these hyphens are ignored)--:00",
"20++++:02++(these plusses are ignored)++:00",
"20: (A comment!) 02 (Another comment!) :00",
"20:02 (A nested (comment!)) :00",
"20:02 (So (many (nested) comments!!!!)) :00",
"20 : 02 : 00.000000",
"20:02",
"20 : 02",
"8:02pm",
"8: 02 pm",
"8:02p.m.",
"8: 02 p.m.",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn hours_only() {
let reference = Time {
hour: 11,
minute: 0,
second: 0.0,
offset: None,
};
for mut s in [
"11am",
"11 am",
"11 - am",
"11 + am",
"11 a.m.",
"11 : 00",
"11:00:00",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn noon() {
let reference = Time {
hour: 12,
minute: 0,
second: 0.0,
offset: None,
};
for mut s in [
"12:00",
"12pm",
"12 pm",
"12 (A comment!) pm",
"12 pm",
"12 p.m.",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn midnight() {
let reference = Time {
hour: 0,
minute: 0,
second: 0.0,
offset: None,
};
for mut s in ["00:00", "12am"] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn offset_hours() {
let reference = Time {
hour: 1,
minute: 23,
second: 0.0,
offset: Some(Offset {
negative: false,
hours: 5,
minutes: 0,
}),
};
for mut s in [
"1:23+5",
"1:23 + 5",
"1:23+05",
"1:23 + 5 : 00",
"1:23+05:00",
"1:23+05:0",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn offset_hours_and_minutes() {
let reference = Time {
hour: 3,
minute: 45,
second: 0.0,
offset: Some(Offset {
negative: false,
hours: 5,
minutes: 35,
}),
};
for mut s in [
"3:45+535",
"3:45-+535",
"03:45+535",
"3 : 45 + 535",
"3:45+0535",
"3:45+5:35",
"3:45+05:35",
"3:45 + 05 : 35",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn offset_minutes() {
let reference = Time {
hour: 3,
minute: 45,
second: 0.0,
offset: Some(Offset {
negative: false,
hours: 0,
minutes: 35,
}),
};
for mut s in [
"3:45+035",
"03:45+035",
"3 : 45 + 035",
"3:45+0035",
"3:45+0:35",
"3:45+00:35",
"3:45 + 00 : 35",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn offset_negative() {
let reference = Time {
hour: 3,
minute: 45,
second: 0.0,
offset: Some(Offset {
negative: true,
hours: 5,
minutes: 35,
}),
};
for mut s in [
"3:45-535",
"03:45-535",
"3 : 45 - 535",
"3:45-0535",
"3:45-5:35",
"3:45-05:35",
"3:45 - 05 : 35",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
}
+14
View File
@@ -0,0 +1,14 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Parse a time zone items
//!
//! The GNU docs state:
//!
//! > Normally, dates are interpreted using the rules of the current time zone,
//! > which in turn are specified by the TZ environment variable, or by a
//! > system default if TZ is not set. To specify a different set of default
//! > time zone rules that apply just to one date, start the date with a string
//! > of the form TZ="rule". The two quote characters (") must be present
//! > in the date, and any quotes or backslashes within rule must be escaped by
//! > a backslash.
+124
View File
@@ -0,0 +1,124 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore wednes
//! The GNU docs state:
//!
//! > The explicit mention of a day of the week will forward the date (only if
//! > necessary) to reach that day of the week in the future.
//! >
//! > Days of the week may be spelled out in full: Sunday, Monday,
//! > Tuesday, Wednesday, Thursday, Friday or Saturday. Days may be
//! > abbreviated to their first three letters, optionally followed by a
//! > period. The special abbreviations Tues for Tuesday, Wednes for
//! > Wednesday and Thur or Thurs for Thursday are also allowed.
//! >
//! > A number may precede a day of the week item to move forward supplementary
//! > weeks. It is best used in expression like third monday. In this
//! > context, last day or next day is also acceptable; they move one week
//! > before or after the day that day by itself would represent.
//! >
//! > A comma following a day of the week item is ignored.
use winnow::{ascii::alpha1, combinator::opt, seq, PResult, Parser};
use super::{ordinal::ordinal, s};
#[derive(PartialEq, Eq, Debug)]
enum Day {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
#[derive(PartialEq, Eq, Debug)]
pub struct Weekday {
offset: i32,
day: Day,
}
pub fn parse(input: &mut &str) -> PResult<Weekday> {
seq!(Weekday {
offset: opt(ordinal).map(|o| o.unwrap_or_default()),
day: day,
})
.parse_next(input)
}
fn day(input: &mut &str) -> PResult<Day> {
s(alpha1)
.verify_map(|s: &str| {
Some(match s {
"monday" | "mon" | "mon." => Day::Monday,
"tuesday" | "tue" | "tue." | "tues" => Day::Tuesday,
"wednesday" | "wed" | "wed." | "wednes" => Day::Wednesday,
"thursday" | "thu" | "thu." | "thur" | "thurs" => Day::Thursday,
"friday" | "fri" | "fri." => Day::Friday,
"saturday" | "sat" | "sat." => Day::Saturday,
"sunday" | "sun" | "sun." => Day::Sunday,
_ => return None,
})
})
.parse_next(input)
}
#[cfg(test)]
mod tests {
use super::{parse, Day, Weekday};
#[test]
fn this_monday() {
for mut s in [
"monday",
"mon",
"mon.",
"this monday",
"this mon",
"this mon.",
"this monday",
"this - monday",
"0 monday",
] {
assert_eq!(
parse(&mut s).unwrap(),
Weekday {
offset: 0,
day: Day::Monday,
}
);
}
}
#[test]
fn next_tuesday() {
for s in ["tuesday", "tue", "tue.", "tues"] {
let s = format!("next {s}");
assert_eq!(
parse(&mut s.as_ref()).unwrap(),
Weekday {
offset: 1,
day: Day::Tuesday,
}
);
}
}
#[test]
fn last_wednesday() {
for s in ["wednesday", "wed", "wed.", "wednesday"] {
let s = format!("last {s}");
assert_eq!(
parse(&mut s.as_ref()).unwrap(),
Weekday {
offset: -1,
day: Day::Wednesday,
}
);
}
}
}
+8 -341
View File
@@ -8,39 +8,22 @@
//! * unix timestamps, e.g., "@12"
//! * relative time to now, e.g. "+1 hour"
//!
use regex::Error as RegexError;
use regex::Regex;
use std::error::Error;
use std::fmt::{self, Display};
// Expose parse_datetime
mod parse_relative_time;
mod parse_timestamp;
use items::Item;
mod parse_time_only_str;
mod parse_weekday;
use chrono::{
DateTime, Datelike, Duration, FixedOffset, Local, LocalResult, MappedLocalTime, NaiveDate,
NaiveDateTime, TimeZone, Timelike,
};
use parse_relative_time::parse_relative_time_at_date;
use parse_timestamp::parse_timestamp;
mod items;
#[derive(Debug, PartialEq)]
pub enum ParseDateTimeError {
InvalidRegex(RegexError),
InvalidInput,
}
impl Display for ParseDateTimeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidRegex(err) => {
write!(f, "Invalid regex for time pattern: {err}")
}
Self::InvalidInput => {
ParseDateTimeError::InvalidInput => {
write!(
f,
"Invalid input string: cannot be parsed as a relative time"
@@ -52,327 +35,11 @@ impl Display for ParseDateTimeError {
impl Error for ParseDateTimeError {}
impl From<RegexError> for ParseDateTimeError {
fn from(err: RegexError) -> Self {
Self::InvalidRegex(err)
}
}
/// Formats that parse input can take.
/// Taken from `touch` coreutils
mod format {
pub const ISO_8601: &str = "%Y-%m-%d";
pub const ISO_8601_NO_SEP: &str = "%Y%m%d";
// US format for calendar date items:
// https://www.gnu.org/software/coreutils/manual/html_node/Calendar-date-items.html
pub const MMDDYYYY_SLASH: &str = "%m/%d/%Y";
pub const MMDDYY_SLASH: &str = "%m/%d/%y";
pub const POSIX_LOCALE: &str = "%a %b %e %H:%M:%S %Y";
pub const YYYYMMDDHHMM_DOT_SS: &str = "%Y%m%d%H%M.%S";
pub const YYYYMMDDHHMMSS: &str = "%Y-%m-%d %H:%M:%S.%f";
pub const YYYYMMDDHHMMS: &str = "%Y-%m-%d %H:%M:%S";
pub const YYYY_MM_DD_HH_MM: &str = "%Y-%m-%d %H:%M";
pub const YYYYMMDDHHMM: &str = "%Y%m%d%H%M";
pub const YYYYMMDDHHMM_OFFSET: &str = "%Y%m%d%H%M %z";
pub const YYYYMMDDHHMM_UTC_OFFSET: &str = "%Y%m%d%H%MUTC%z";
pub const YYYYMMDDHHMM_ZULU_OFFSET: &str = "%Y%m%d%H%MZ%z";
pub const YYYYMMDDHHMM_HYPHENATED_OFFSET: &str = "%Y-%m-%d %H:%M %z";
pub const YYYYMMDDHHMMSS_HYPHENATED_OFFSET: &str = "%Y-%m-%d %H:%M:%S %#z";
pub const YYYYMMDDHHMMSS_HYPHENATED_ZULU: &str = "%Y-%m-%d %H:%M:%SZ";
pub const YYYYMMDDHHMMSS_T_SEP_HYPHENATED_OFFSET: &str = "%Y-%m-%dT%H:%M:%S%#z";
pub const YYYYMMDDHHMMSS_T_SEP_HYPHENATED_ZULU: &str = "%Y-%m-%dT%H:%M:%SZ";
pub const YYYYMMDDHHMMSS_T_SEP_HYPHENATED_SPACE_OFFSET: &str = "%Y-%m-%dT%H:%M:%S %#z";
pub const YYYYMMDDHHMMS_T_SEP: &str = "%Y-%m-%dT%H:%M:%S";
pub const UTC_OFFSET: &str = "UTC%#z";
pub const ZULU_OFFSET: &str = "Z%#z";
pub const NAKED_OFFSET: &str = "%#z";
/// Whether the pattern ends in the character `Z`.
pub(crate) fn is_zulu(pattern: &str) -> bool {
pattern.ends_with('Z')
}
/// Patterns for datetimes with timezones.
///
/// These are in decreasing order of length. The same pattern may
/// appear multiple times with different lengths if the pattern
/// accepts input strings of different lengths. For example, the
/// specifier `%#z` accepts two-digit time zone offsets (`+00`)
/// and four-digit time zone offsets (`+0000`).
pub(crate) const PATTERNS_TZ: [(&str, usize); 9] = [
(YYYYMMDDHHMMSS_HYPHENATED_OFFSET, 25),
(YYYYMMDDHHMMSS_T_SEP_HYPHENATED_SPACE_OFFSET, 25),
(YYYYMMDDHHMMSS_T_SEP_HYPHENATED_OFFSET, 24),
(YYYYMMDDHHMMSS_HYPHENATED_OFFSET, 23),
(YYYYMMDDHHMMSS_T_SEP_HYPHENATED_OFFSET, 22),
(YYYYMMDDHHMM_HYPHENATED_OFFSET, 22),
(YYYYMMDDHHMM_UTC_OFFSET, 20),
(YYYYMMDDHHMM_OFFSET, 18),
(YYYYMMDDHHMM_ZULU_OFFSET, 18),
];
/// Patterns for datetimes without timezones.
///
/// These are in decreasing order of length.
pub(crate) const PATTERNS_NO_TZ: [(&str, usize); 9] = [
(YYYYMMDDHHMMSS, 29),
(POSIX_LOCALE, 24),
(YYYYMMDDHHMMSS_HYPHENATED_ZULU, 20),
(YYYYMMDDHHMMSS_T_SEP_HYPHENATED_ZULU, 20),
(YYYYMMDDHHMMS_T_SEP, 19),
(YYYYMMDDHHMMS, 19),
(YYYY_MM_DD_HH_MM, 16),
(YYYYMMDDHHMM_DOT_SS, 15),
(YYYYMMDDHHMM, 12),
];
/// Patterns for dates with neither times nor timezones.
///
/// These are in decreasing order of length. The same pattern may
/// appear multiple times with different lengths if the pattern
/// accepts input strings of different lengths. For example, the
/// specifier `%m` accepts one-digit month numbers (like `2`) and
/// two-digit month numbers (like `02` or `12`).
pub(crate) const PATTERNS_DATE_NO_TZ: [(&str, usize); 8] = [
(ISO_8601, 10),
(MMDDYYYY_SLASH, 10),
(ISO_8601, 9),
(MMDDYYYY_SLASH, 9),
(ISO_8601, 8),
(MMDDYY_SLASH, 8),
(MMDDYYYY_SLASH, 8),
(ISO_8601_NO_SEP, 8),
];
/// Patterns for lone timezone offsets.
///
/// These are in decreasing order of length. The same pattern may
/// appear multiple times with different lengths if the pattern
/// accepts input strings of different lengths. For example, the
/// specifier `%#z` accepts two-digit time zone offsets (`+00`)
/// and four-digit time zone offsets (`+0000`).
pub(crate) const PATTERNS_OFFSET: [(&str, usize); 9] = [
(UTC_OFFSET, 9),
(UTC_OFFSET, 8),
(ZULU_OFFSET, 7),
(UTC_OFFSET, 6),
(ZULU_OFFSET, 6),
(NAKED_OFFSET, 6),
(NAKED_OFFSET, 5),
(ZULU_OFFSET, 4),
(NAKED_OFFSET, 3),
];
}
/// Parses a time string and returns a `DateTime` representing the
/// absolute time of the string.
///
/// # Arguments
///
/// * `s` - A string slice representing the time.
///
/// # Examples
///
/// ```
/// use chrono::{DateTime, Utc, TimeZone};
/// let time = parse_datetime::parse_datetime("2023-06-03 12:00:01Z");
/// assert_eq!(time.unwrap(), Utc.with_ymd_and_hms(2023, 06, 03, 12, 00, 01).unwrap());
/// ```
///
///
/// # Returns
///
/// * `Ok(DateTime<FixedOffset>)` - If the input string can be parsed as a time
/// * `Err(ParseDateTimeError)` - If the input string cannot be parsed as a relative time
///
/// # Errors
///
/// This function will return `Err(ParseDateTimeError::InvalidInput)` if the input string
/// cannot be parsed as a relative time.
pub fn parse_datetime<S: AsRef<str> + Clone>(
s: S,
) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
parse_datetime_at_date(Local::now(), s)
}
/// Parses a time string at a specific date and returns a `DateTime` representing the
/// absolute time of the string.
///
/// # Arguments
///
/// * date - The date represented in local time
/// * `s` - A string slice representing the time.
///
/// # Examples
///
/// ```
/// use chrono::{Duration, Local};
/// use parse_datetime::parse_datetime_at_date;
///
/// let now = Local::now();
/// let after = parse_datetime_at_date(now, "+3 days");
///
/// assert_eq!(
/// (now + Duration::days(3)).naive_utc(),
/// after.unwrap().naive_utc()
/// );
/// ```
///
/// # Returns
///
/// * `Ok(DateTime<FixedOffset>)` - If the input string can be parsed as a time
/// * `Err(ParseDateTimeError)` - If the input string cannot be parsed as a relative time
///
/// # Errors
///
/// This function will return `Err(ParseDateTimeError::InvalidInput)` if the input string
/// cannot be parsed as a relative time.
pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
date: DateTime<Local>,
s: S,
) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
// TODO: Replace with a proper customiseable parsing solution using `nom`, `grmtools`, or
// similar
// Try to parse a reference date first. Try parsing from longest
// pattern to shortest pattern. If a reference date can be parsed,
// then try to parse a time delta from the remaining slice. If no
// reference date could be parsed, then try to parse the entire
// string as a time delta. If no time delta could be parsed,
// return an error.
let (ref_date, n) = if let Some((ref_date, n)) = parse_reference_date(date, s.as_ref()) {
(ref_date, n)
} else {
let tz = TimeZone::from_offset(date.offset());
match date.naive_local().and_local_timezone(tz) {
MappedLocalTime::Single(ref_date) => (ref_date, 0),
_ => return Err(ParseDateTimeError::InvalidInput),
}
};
parse_relative_time_at_date(ref_date, &s.as_ref()[n..])
}
/// Parse an absolute datetime from a prefix of s, if possible.
///
/// Try to parse the longest possible absolute datetime at the beginning
/// of string `s`. Return the parsed datetime and the index in `s` at
/// which the datetime ended.
fn parse_reference_date<S>(date: DateTime<Local>, s: S) -> Option<(DateTime<FixedOffset>, usize)>
where
S: AsRef<str>,
{
// HACK: if the string ends with a single digit preceded by a + or -
// sign, then insert a 0 between the sign and the digit to make it
// possible for `chrono` to parse it.
let pattern = Regex::new(r"([\+-])(\d)$").unwrap();
let tmp_s = pattern.replace(s.as_ref(), "${1}0${2}");
for (fmt, n) in format::PATTERNS_TZ {
if tmp_s.len() >= n {
if let Ok(parsed) = DateTime::parse_from_str(&tmp_s[0..n], fmt) {
if tmp_s == s.as_ref() {
return Some((parsed, n));
}
return Some((parsed, n - 1));
}
}
}
// Parse formats with no offset, assume local time
for (fmt, n) in format::PATTERNS_NO_TZ {
if s.as_ref().len() >= n {
if let Ok(parsed) = NaiveDateTime::parse_from_str(&s.as_ref()[0..n], fmt) {
// Special case: `chrono` can only parse a datetime like
// `2000-01-01 01:23:45Z` as a naive datetime, so we
// manually force it to be in UTC.
if format::is_zulu(fmt) {
match FixedOffset::east_opt(0)
.unwrap()
.from_local_datetime(&parsed)
{
MappedLocalTime::Single(datetime) => return Some((datetime, n)),
_ => return None,
}
} else if let Ok(dt) = naive_dt_to_fixed_offset(date, parsed) {
return Some((dt, n));
}
}
}
}
// parse weekday
if let Some(weekday) = parse_weekday::parse_weekday(s.as_ref()) {
let mut beginning_of_day = date
.with_hour(0)
.unwrap()
.with_minute(0)
.unwrap()
.with_second(0)
.unwrap()
.with_nanosecond(0)
.unwrap();
while beginning_of_day.weekday() != weekday {
beginning_of_day += Duration::days(1);
}
let dt = DateTime::<FixedOffset>::from(beginning_of_day);
return Some((dt, s.as_ref().len()));
}
// Parse epoch seconds
if let Ok(timestamp) = parse_timestamp(s.as_ref()) {
if let Some(timestamp_date) = DateTime::from_timestamp(timestamp, 0) {
return Some((timestamp_date.into(), s.as_ref().len()));
}
}
// Parse date only formats - assume midnight local timezone
for (fmt, n) in format::PATTERNS_DATE_NO_TZ {
if s.as_ref().len() >= n {
if let Ok(parsed) = NaiveDate::parse_from_str(&s.as_ref()[0..n], fmt) {
let datetime = parsed.and_hms_opt(0, 0, 0).unwrap();
if let Ok(dt) = naive_dt_to_fixed_offset(date, datetime) {
return Some((dt, n));
}
}
}
}
// Parse offsets. chrono doesn't provide any functionality to parse
// offsets, so instead we replicate parse_date behaviour by getting
// the current date with local, and create a date time string at midnight,
// before trying offset suffixes
let ts = format!("{}0000{}", date.format("%Y%m%d"), tmp_s.as_ref());
for (fmt, n) in format::PATTERNS_OFFSET {
if ts.len() == n + 12 {
let f = format::YYYYMMDDHHMM.to_owned() + fmt;
if let Ok(parsed) = DateTime::parse_from_str(&ts, &f) {
if tmp_s == s.as_ref() {
return Some((parsed, n));
}
return Some((parsed, n - 1));
}
}
}
// parse time only dates
if let Some(date_time) = parse_time_only_str::parse_time_only(date, s.as_ref()) {
return Some((date_time, s.as_ref().len()));
}
None
}
// Convert NaiveDateTime to DateTime<FixedOffset> by assuming the offset
// is local time
fn naive_dt_to_fixed_offset(
local: DateTime<Local>,
dt: NaiveDateTime,
) -> Result<DateTime<FixedOffset>, ()> {
match local.offset().from_local_datetime(&dt) {
LocalResult::Single(dt) => Ok(dt),
_ => Err(()),
pub fn parse_datetime(input: &str) -> Result<Vec<Item>, ParseDateTimeError> {
let input = input.to_ascii_lowercase();
match items::parse(&mut input.as_ref()) {
Some(x) => Ok(x),
None => Err(ParseDateTimeError::InvalidInput),
}
}