From 6202705eb621dc9edaec0eca2556d665bac9140e Mon Sep 17 00:00:00 2001 From: Nico Mandery Date: Wed, 5 Aug 2020 02:44:52 +0200 Subject: [PATCH] parse most common date formats using dtparse crate (#2303) * use dtparse crate to parse most common date formats * use dtparse crate to parse most common date formats - cargo fmt --- crates/nu-cli/Cargo.toml | 3 +- .../nu-cli/src/commands/str_/to_datetime.rs | 92 ++++++++++++++----- 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 46a216ab08..a853ffcbe9 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -34,8 +34,9 @@ codespan-reporting = "0.9.5" csv = "1.1" ctrlc = {version = "3.1.4", optional = true} derive-new = "0.5.8" -directories = {version = "2.0.2", optional = true} +directories = {version = "2.0.2", optional = true} dirs = {version = "2.0.2", optional = true} +dtparse = "1.1.0" dunce = "1.0.1" eml-parser = "0.1.0" filesize = "0.2.0" diff --git a/crates/nu-cli/src/commands/str_/to_datetime.rs b/crates/nu-cli/src/commands/str_/to_datetime.rs index b4e9944593..4adce2add6 100644 --- a/crates/nu-cli/src/commands/str_/to_datetime.rs +++ b/crates/nu-cli/src/commands/str_/to_datetime.rs @@ -8,7 +8,7 @@ use nu_protocol::{ use nu_source::{Tag, Tagged}; use nu_value_ext::ValueExt; -use chrono::DateTime; +use chrono::{DateTime, FixedOffset, LocalResult, Offset, TimeZone}; #[derive(Deserialize)] struct Arguments { @@ -51,11 +51,23 @@ impl WholeStreamCommand for SubCommand { } fn examples(&self) -> Vec { - vec![Example { - description: "Convert to datetime", - example: "echo '16.11.1984 8:00 am +0000' | str to-datetime", - result: None, - }] + vec![ + Example { + description: "Convert to datetime", + example: "echo '16.11.1984 8:00 am +0000' | str to-datetime", + result: None, + }, + Example { + description: "Convert to datetime", + example: "echo '2020-08-04T16:39:18+00:00' | str to-datetime", + result: None, + }, + Example { + description: "Convert to datetime using a custom format", + example: "echo '20200904_163918+0000' | str to-datetime -f '%Y%m%d_%H%M%S%z'", + result: None, + }, + ] } } @@ -73,9 +85,9 @@ async fn operate( let column_paths: Vec<_> = rest; let options = if let Some(Tagged { item: fmt, .. }) = format { - DatetimeFormat(fmt) + Some(DatetimeFormat(fmt)) } else { - DatetimeFormat(String::from("%d.%m.%Y %H:%M %P %z")) + None }; Ok(input @@ -102,23 +114,49 @@ async fn operate( fn action( input: &Value, - options: &DatetimeFormat, + options: &Option, tag: impl Into, ) -> Result { match &input.value { UntaggedValue::Primitive(Primitive::Line(s)) | UntaggedValue::Primitive(Primitive::String(s)) => { - let dt = &options.0; - - let out = match DateTime::parse_from_str(s, dt) { - Ok(d) => UntaggedValue::date(d), - Err(reason) => { - return Err(ShellError::labeled_error( - "could not parse as datetime", - reason.to_string(), - tag.into().span, - )) - } + let out = match options { + Some(dt) => match DateTime::parse_from_str(s, &dt.0) { + Ok(d) => UntaggedValue::date(d), + Err(reason) => { + return Err(ShellError::labeled_error( + format!("could not parse as datetime using format '{}'", dt.0), + reason.to_string(), + tag.into().span, + )) + } + }, + None => match dtparse::parse(s) { + Ok((native_dt, fixed_offset)) => { + let offset = match fixed_offset { + Some(fo) => fo, + None => FixedOffset::east(0).fix(), + }; + match offset.from_local_datetime(&native_dt) { + LocalResult::Single(d) => UntaggedValue::date(d), + LocalResult::Ambiguous(d, _) => UntaggedValue::date(d), + LocalResult::None => { + return Err(ShellError::labeled_error( + "could not convert to a timezone-aware datetime", + "local time representation is invalid", + tag.into().span, + )) + } + } + } + Err(reason) => { + return Err(ShellError::labeled_error( + "could not parse as datetime", + reason.to_string(), + tag.into().span, + )) + } + }, }; Ok(out.into_value(tag)) @@ -152,7 +190,7 @@ mod tests { fn takes_a_date_format() { let date_str = string("16.11.1984 8:00 am +0000"); - let fmt_options = DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()); + let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string())); let actual = action(&date_str, &fmt_options, Tag::unknown()).unwrap(); @@ -162,11 +200,21 @@ mod tests { } } + #[test] + fn takes_iso8601_date_format() { + let date_str = string("2020-08-04T16:39:18+00:00"); + let actual = action(&date_str, &None, Tag::unknown()).unwrap(); + match actual.value { + UntaggedValue::Primitive(Primitive::Date(_)) => {} + _ => panic!("Didn't convert to date"), + } + } + #[test] fn communicates_parsing_error_given_an_invalid_datetimelike_string() { let date_str = string("16.11.1984 8:00 am Oops0000"); - let fmt_options = DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()); + let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string())); let actual = action(&date_str, &fmt_options, Tag::unknown());