Skip to content

Commit 8aee979

Browse files
committed
Parse reference date and delta from same string
Change the `parse_datetime()` function so that it parses both a reference date and a time delta from one string. The new implementation attempts to parse the datetime from the longest possible prefix of the string. The remainder of the string is parsed as the time delta. This allows us to parse more combinations of reference dates and time deltas more easily. Fixes #104
1 parent f1fada6 commit 8aee979

File tree

1 file changed

+124
-28
lines changed

1 file changed

+124
-28
lines changed

src/lib.rs

Lines changed: 124 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,34 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
234234
// TODO: Replace with a proper customiseable parsing solution using `nom`, `grmtools`, or
235235
// similar
236236

237-
// Formats with offsets don't require NaiveDateTime workaround
238-
//
237+
// Try to parse a reference date first. Try parsing from longest
238+
// pattern to shortest pattern. If a reference date can be parsed,
239+
// then try to parse a time delta from the remaining slice. If no
240+
// reference date could be parsed, then try to parse the entire
241+
// string as a time delta. If no time delta could be parsed,
242+
// return an error.
243+
let (ref_date, n) = match parse_reference_date(date, s.as_ref()) {
244+
Some((ref_date, n)) => (ref_date, n),
245+
None => {
246+
let tz = TimeZone::from_offset(date.offset());
247+
match date.naive_local().and_local_timezone(tz) {
248+
MappedLocalTime::Single(ref_date) => (ref_date, 0),
249+
_ => return Err(ParseDateTimeError::InvalidInput),
250+
}
251+
}
252+
};
253+
parse_relative_time_at_date(ref_date, &s.as_ref()[n..])
254+
}
255+
256+
/// Parse an absolute datetime from a prefix of s, if possible.
257+
///
258+
/// Try to parse the longest possible absolute datetime at the beginning
259+
/// of string `s`. Return the parsed datetime and the index in `s` at
260+
/// which the datetime ended.
261+
fn parse_reference_date<S>(date: DateTime<Local>, s: S) -> Option<(DateTime<FixedOffset>, usize)>
262+
where
263+
S: AsRef<str>,
264+
{
239265
// HACK: if the string ends with a single digit preceded by a + or -
240266
// sign, then insert a 0 between the sign and the digit to make it
241267
// possible for `chrono` to parse it.
@@ -244,7 +270,11 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
244270
for (fmt, n) in format::PATTERNS_TZ {
245271
if tmp_s.len() >= n {
246272
if let Ok(parsed) = DateTime::parse_from_str(&tmp_s[0..n], fmt) {
247-
return Ok(parsed);
273+
if tmp_s == s.as_ref() {
274+
return Some((parsed, n));
275+
} else {
276+
return Some((parsed, n - 1));
277+
}
248278
}
249279
}
250280
}
@@ -261,11 +291,11 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
261291
.unwrap()
262292
.from_local_datetime(&parsed)
263293
{
264-
MappedLocalTime::Single(datetime) => return Ok(datetime),
265-
_ => return Err(ParseDateTimeError::InvalidInput),
294+
MappedLocalTime::Single(datetime) => return Some((datetime, n)),
295+
_ => return None,
266296
}
267297
} else if let Ok(dt) = naive_dt_to_fixed_offset(date, parsed) {
268-
return Ok(dt);
298+
return Some((dt, n));
269299
}
270300
}
271301
}
@@ -289,13 +319,13 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
289319

290320
let dt = DateTime::<FixedOffset>::from(beginning_of_day);
291321

292-
return Ok(dt);
322+
return Some((dt, s.as_ref().len()));
293323
}
294324

295325
// Parse epoch seconds
296326
if let Ok(timestamp) = parse_timestamp(s.as_ref()) {
297327
if let Some(timestamp_date) = DateTime::from_timestamp(timestamp, 0) {
298-
return Ok(timestamp_date.into());
328+
return Some((timestamp_date.into(), s.as_ref().len()));
299329
}
300330
}
301331

@@ -305,7 +335,7 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
305335
if let Ok(parsed) = NaiveDate::parse_from_str(&s.as_ref()[0..n], fmt) {
306336
let datetime = parsed.and_hms_opt(0, 0, 0).unwrap();
307337
if let Ok(dt) = naive_dt_to_fixed_offset(date, datetime) {
308-
return Ok(dt);
338+
return Some((dt, n));
309339
}
310340
}
311341
}
@@ -320,25 +350,21 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
320350
if ts.len() == n + 12 {
321351
let f = format::YYYYMMDDHHMM.to_owned() + fmt;
322352
if let Ok(parsed) = DateTime::parse_from_str(&ts, &f) {
323-
return Ok(parsed);
353+
if tmp_s == s.as_ref() {
354+
return Some((parsed, n));
355+
} else {
356+
return Some((parsed, n - 1));
357+
}
324358
}
325359
}
326360
}
327361

328-
// Parse relative time.
329-
if let Ok(datetime) = parse_relative_time_at_date(date, s.as_ref()) {
330-
return Ok(DateTime::<FixedOffset>::from(datetime));
331-
}
332-
333362
// parse time only dates
334363
if let Some(date_time) = parse_time_only_str::parse_time_only(date, s.as_ref()) {
335-
return Ok(date_time);
364+
return Some((date_time, s.as_ref().len()));
336365
}
337366

338-
// Default parse and failure
339-
s.as_ref()
340-
.parse()
341-
.map_err(|_| (ParseDateTimeError::InvalidInput))
367+
None
342368
}
343369

344370
// Convert NaiveDateTime to DateTime<FixedOffset> by assuming the offset
@@ -664,14 +690,10 @@ mod tests {
664690
assert!(crate::parse_datetime("bogus +1 day").is_err());
665691
}
666692

667-
// TODO Re-enable this when we parse the absolute datetime and the
668-
// time delta separately, see
669-
// <https://github.com/uutils/parse_datetime/issues/104>.
670-
//
671-
// #[test]
672-
// fn test_parse_invalid_delta() {
673-
// assert!(crate::parse_datetime("1997-01-01 bogus").is_err());
674-
// }
693+
#[test]
694+
fn test_parse_invalid_delta() {
695+
assert!(crate::parse_datetime("1997-01-01 bogus").is_err());
696+
}
675697

676698
#[test]
677699
fn test_parse_datetime_tz_nodelta() {
@@ -743,6 +765,80 @@ mod tests {
743765
}
744766
}
745767

768+
#[test]
769+
fn test_parse_datetime_tz_delta() {
770+
std::env::set_var("TZ", "UTC0");
771+
772+
// 1998-01-01
773+
let expected = chrono::NaiveDate::from_ymd_opt(1998, 1, 1)
774+
.unwrap()
775+
.and_hms_opt(0, 0, 0)
776+
.unwrap()
777+
.and_utc()
778+
.fixed_offset();
779+
780+
for s in [
781+
"1997-01-01 00:00:00 +0000 +1 year",
782+
"1997-01-01 00:00:00 +00 +1 year",
783+
"199701010000 +0000 +1 year",
784+
"199701010000UTC+0000 +1 year",
785+
"199701010000Z+0000 +1 year",
786+
"1997-01-01T00:00:00Z +1 year",
787+
"1997-01-01 00:00 +0000 +1 year",
788+
"1997-01-01 00:00:00 +0000 +1 year",
789+
"1997-01-01T00:00:00+0000 +1 year",
790+
"1997-01-01T00:00:00+00 +1 year",
791+
] {
792+
let actual = crate::parse_datetime(s).unwrap();
793+
assert_eq!(actual, expected);
794+
}
795+
}
796+
797+
#[test]
798+
fn test_parse_datetime_notz_delta() {
799+
std::env::set_var("TZ", "UTC0");
800+
let expected = chrono::NaiveDate::from_ymd_opt(1998, 1, 1)
801+
.unwrap()
802+
.and_hms_opt(0, 0, 0)
803+
.unwrap()
804+
.and_utc()
805+
.fixed_offset();
806+
807+
for s in [
808+
"1997-01-01 00:00:00.000000000 +1 year",
809+
"Wed Jan 1 00:00:00 1997 +1 year",
810+
"1997-01-01T00:00:00 +1 year",
811+
"1997-01-01 00:00:00 +1 year",
812+
"1997-01-01 00:00 +1 year",
813+
"199701010000.00 +1 year",
814+
"199701010000 +1 year",
815+
] {
816+
let actual = crate::parse_datetime(s).unwrap();
817+
assert_eq!(actual, expected);
818+
}
819+
}
820+
821+
#[test]
822+
fn test_parse_date_notz_delta() {
823+
std::env::set_var("TZ", "UTC0");
824+
let expected = chrono::NaiveDate::from_ymd_opt(1998, 1, 1)
825+
.unwrap()
826+
.and_hms_opt(0, 0, 0)
827+
.unwrap()
828+
.and_utc()
829+
.fixed_offset();
830+
831+
for s in [
832+
"1997-01-01 +1 year",
833+
"19970101 +1 year",
834+
"01/01/1997 +1 year",
835+
"01/01/97 +1 year",
836+
] {
837+
let actual = crate::parse_datetime(s).unwrap();
838+
assert_eq!(actual, expected);
839+
}
840+
}
841+
746842
#[test]
747843
fn test_time_only() {
748844
use chrono::{FixedOffset, Local};

0 commit comments

Comments
 (0)