Skip to content

Commit e7a7a0b

Browse files
authored
Merge pull request #110 from jfinkels/parse-ref-date-and-delta
Parse reference datetime and timedelta from the same string
2 parents ca6a39c + d4353d0 commit e7a7a0b

File tree

1 file changed

+128
-30
lines changed

1 file changed

+128
-30
lines changed

src/lib.rs

+128-30
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ mod format {
8080
pub const YYYYMMDDHHMMSS_HYPHENATED_OFFSET: &str = "%Y-%m-%d %H:%M:%S %#z";
8181
pub const YYYYMMDDHHMMSS_HYPHENATED_ZULU: &str = "%Y-%m-%d %H:%M:%SZ";
8282
pub const YYYYMMDDHHMMSS_T_SEP_HYPHENATED_OFFSET: &str = "%Y-%m-%dT%H:%M:%S%#z";
83+
pub const YYYYMMDDHHMMSS_T_SEP_HYPHENATED_ZULU: &str = "%Y-%m-%dT%H:%M:%SZ";
8384
pub const YYYYMMDDHHMMSS_T_SEP_HYPHENATED_SPACE_OFFSET: &str = "%Y-%m-%dT%H:%M:%S %#z";
8485
pub const YYYYMMDDHHMMS_T_SEP: &str = "%Y-%m-%dT%H:%M:%S";
8586
pub const UTC_OFFSET: &str = "UTC%#z";
@@ -88,7 +89,7 @@ mod format {
8889

8990
/// Whether the pattern ends in the character `Z`.
9091
pub(crate) fn is_zulu(pattern: &str) -> bool {
91-
pattern == YYYYMMDDHHMMSS_HYPHENATED_ZULU
92+
pattern.ends_with('Z')
9293
}
9394

9495
/// Patterns for datetimes with timezones.
@@ -113,10 +114,11 @@ mod format {
113114
/// Patterns for datetimes without timezones.
114115
///
115116
/// These are in decreasing order of length.
116-
pub(crate) const PATTERNS_NO_TZ: [(&str, usize); 8] = [
117+
pub(crate) const PATTERNS_NO_TZ: [(&str, usize); 9] = [
117118
(YYYYMMDDHHMMSS, 29),
118119
(POSIX_LOCALE, 24),
119120
(YYYYMMDDHHMMSS_HYPHENATED_ZULU, 20),
121+
(YYYYMMDDHHMMSS_T_SEP_HYPHENATED_ZULU, 20),
120122
(YYYYMMDDHHMMS_T_SEP, 19),
121123
(YYYYMMDDHHMMS, 19),
122124
(YYYY_MM_DD_HH_MM, 16),
@@ -232,8 +234,34 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
232234
// TODO: Replace with a proper customiseable parsing solution using `nom`, `grmtools`, or
233235
// similar
234236

235-
// Formats with offsets don't require NaiveDateTime workaround
236-
//
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+
{
237265
// HACK: if the string ends with a single digit preceded by a + or -
238266
// sign, then insert a 0 between the sign and the digit to make it
239267
// possible for `chrono` to parse it.
@@ -242,7 +270,11 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
242270
for (fmt, n) in format::PATTERNS_TZ {
243271
if tmp_s.len() >= n {
244272
if let Ok(parsed) = DateTime::parse_from_str(&tmp_s[0..n], fmt) {
245-
return Ok(parsed);
273+
if tmp_s == s.as_ref() {
274+
return Some((parsed, n));
275+
} else {
276+
return Some((parsed, n - 1));
277+
}
246278
}
247279
}
248280
}
@@ -259,11 +291,11 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
259291
.unwrap()
260292
.from_local_datetime(&parsed)
261293
{
262-
MappedLocalTime::Single(datetime) => return Ok(datetime),
263-
_ => return Err(ParseDateTimeError::InvalidInput),
294+
MappedLocalTime::Single(datetime) => return Some((datetime, n)),
295+
_ => return None,
264296
}
265297
} else if let Ok(dt) = naive_dt_to_fixed_offset(date, parsed) {
266-
return Ok(dt);
298+
return Some((dt, n));
267299
}
268300
}
269301
}
@@ -287,13 +319,13 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
287319

288320
let dt = DateTime::<FixedOffset>::from(beginning_of_day);
289321

290-
return Ok(dt);
322+
return Some((dt, s.as_ref().len()));
291323
}
292324

293325
// Parse epoch seconds
294326
if let Ok(timestamp) = parse_timestamp(s.as_ref()) {
295327
if let Some(timestamp_date) = DateTime::from_timestamp(timestamp, 0) {
296-
return Ok(timestamp_date.into());
328+
return Some((timestamp_date.into(), s.as_ref().len()));
297329
}
298330
}
299331

@@ -303,7 +335,7 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
303335
if let Ok(parsed) = NaiveDate::parse_from_str(&s.as_ref()[0..n], fmt) {
304336
let datetime = parsed.and_hms_opt(0, 0, 0).unwrap();
305337
if let Ok(dt) = naive_dt_to_fixed_offset(date, datetime) {
306-
return Ok(dt);
338+
return Some((dt, n));
307339
}
308340
}
309341
}
@@ -318,25 +350,21 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
318350
if ts.len() == n + 12 {
319351
let f = format::YYYYMMDDHHMM.to_owned() + fmt;
320352
if let Ok(parsed) = DateTime::parse_from_str(&ts, &f) {
321-
return Ok(parsed);
353+
if tmp_s == s.as_ref() {
354+
return Some((parsed, n));
355+
} else {
356+
return Some((parsed, n - 1));
357+
}
322358
}
323359
}
324360
}
325361

326-
// Parse relative time.
327-
if let Ok(datetime) = parse_relative_time_at_date(date, s.as_ref()) {
328-
return Ok(DateTime::<FixedOffset>::from(datetime));
329-
}
330-
331362
// parse time only dates
332363
if let Some(date_time) = parse_time_only_str::parse_time_only(date, s.as_ref()) {
333-
return Ok(date_time);
364+
return Some((date_time, s.as_ref().len()));
334365
}
335366

336-
// Default parse and failure
337-
s.as_ref()
338-
.parse()
339-
.map_err(|_| (ParseDateTimeError::InvalidInput))
367+
None
340368
}
341369

342370
// Convert NaiveDateTime to DateTime<FixedOffset> by assuming the offset
@@ -662,14 +690,10 @@ mod tests {
662690
assert!(crate::parse_datetime("bogus +1 day").is_err());
663691
}
664692

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

674698
#[test]
675699
fn test_parse_datetime_tz_nodelta() {
@@ -741,6 +765,80 @@ mod tests {
741765
}
742766
}
743767

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+
744842
#[test]
745843
fn test_time_only() {
746844
use chrono::{FixedOffset, Local};

0 commit comments

Comments
 (0)