Date parse refactor (#4661)

* More flexible and DRY datetime parsing

* Update error messages

* cargo fmt

* clippy

* Add DatetimeParseError
This commit is contained in:
Jonathan Moore 2022-02-27 19:21:46 -06:00 committed by GitHub
parent 0f437589fc
commit ef70c8dbe4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 75 additions and 87 deletions

View file

@ -1,4 +1,4 @@
use chrono::{DateTime, FixedOffset, Local, LocalResult, Offset, TimeZone, Utc};
use chrono::{DateTime, FixedOffset, Local, TimeZone, Utc};
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::ast::CellPath;
@ -8,6 +8,7 @@ use nu_protocol::{
};
use crate::generate_strftime_list;
use crate::parse_date_from_string;
struct Arguments {
timezone: Option<Spanned<String>>,
@ -205,7 +206,7 @@ fn action(
head: Span,
) -> Value {
match input {
Value::String { val: s, span, .. } => {
Value::String { val: s, span } => {
let ts = s.parse::<i64>();
// if timezone if specified, first check if the input is a timestamp.
if let Some(tz) = timezone {
@ -253,7 +254,7 @@ fn action(
return stampout;
}
};
// if it's not, continue and negelect the timezone option.
// if it's not, continue and default to the system's local timezone.
let out = match dateformat {
Some(dt) => match DateTime::parse_from_str(s, &dt.0) {
Ok(d) => Value::Date { val: d, span: head },
@ -267,35 +268,15 @@ fn action(
}
}
},
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) => Value::Date { val: d, span: head },
LocalResult::Ambiguous(d, _) => Value::Date { val: d, span: head },
LocalResult::None => {
return Value::Error {
error: ShellError::CantConvert(
"could not convert to a timezone-aware datetime"
.to_string(),
"local time representation is invalid".to_string(),
head,
),
}
}
}
}
Err(_) => {
return Value::Error {
error: ShellError::UnsupportedInput(
"Cannot convert input string as datetime. Might be missing timezone or offset".to_string(),
*span,
),
}
}
// Tries to automatically parse the date
// (i.e. without a format string)
// and assumes the system's local timezone if none is specified
None => match parse_date_from_string(s, *span) {
Ok(date) => Value::Date {
val: date,
span: *span,
},
Err(err) => err,
},
};

View file

@ -3,10 +3,10 @@ use nu_engine::CallExt;
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Example, PipelineData, Signature, Span, SyntaxShape, Value,
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value,
};
use super::utils::{parse_date_from_string, unsupported_input_error};
use super::utils::parse_date_from_string;
#[derive(Clone)]
pub struct SubCommand;
@ -96,7 +96,7 @@ fn format_helper(value: Value, formatter: &str, span: Span) -> Value {
val,
span: val_span,
} => {
let dt = parse_date_from_string(val, val_span);
let dt = parse_date_from_string(&val, val_span);
match dt {
Ok(x) => Value::String {
val: x.format(formatter).to_string(),
@ -112,7 +112,9 @@ fn format_helper(value: Value, formatter: &str, span: Span) -> Value {
span,
}
}
_ => unsupported_input_error(span),
_ => Value::Error {
error: ShellError::DatetimeParseError(span),
},
}
}
@ -126,7 +128,7 @@ fn format_helper_rfc2822(value: Value, span: Span) -> Value {
val,
span: val_span,
} => {
let dt = parse_date_from_string(val, val_span);
let dt = parse_date_from_string(&val, val_span);
match dt {
Ok(x) => Value::String {
val: x.to_rfc2822(),
@ -142,7 +144,9 @@ fn format_helper_rfc2822(value: Value, span: Span) -> Value {
span,
}
}
_ => unsupported_input_error(span),
_ => Value::Error {
error: ShellError::DatetimeParseError(span),
},
}
}

View file

@ -63,7 +63,7 @@ fn helper(value: Value, head: Span) -> Value {
val,
span: val_span,
} => {
let dt = parse_date_from_string(val, val_span);
let dt = parse_date_from_string(&val, val_span);
match dt {
Ok(x) => Value::String {
val: humanize_date(x),
@ -77,10 +77,7 @@ fn helper(value: Value, head: Span) -> Value {
span: head,
},
_ => Value::Error {
error: ShellError::UnsupportedInput(
String::from("Date cannot be parsed / date format is not supported"),
head,
),
error: ShellError::DatetimeParseError(head),
},
}
}

View file

@ -16,3 +16,4 @@ pub use list_timezone::SubCommand as DateListTimezones;
pub use now::SubCommand as DateNow;
pub use to_table::SubCommand as DateToTable;
pub use to_timezone::SubCommand as DateToTimezone;
pub(crate) use utils::parse_date_from_string;

View file

@ -1,8 +1,10 @@
use crate::date::utils::{parse_date_from_string, unsupported_input_error};
use crate::date::utils::parse_date_from_string;
use chrono::{DateTime, Datelike, FixedOffset, Local, Timelike};
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{Category, Example, PipelineData, Signature, Span, Value};
use nu_protocol::{
Category, Example, PipelineData, ShellError::DatetimeParseError, Signature, Span, Value,
};
#[derive(Clone)]
pub struct SubCommand;
@ -140,7 +142,7 @@ fn helper(val: Value, head: Span) -> Value {
val,
span: val_span,
} => {
let date = parse_date_from_string(val, val_span);
let date = parse_date_from_string(&val, val_span);
parse_date_into_table(date, head)
}
Value::Nothing { span: _ } => {
@ -149,7 +151,9 @@ fn helper(val: Value, head: Span) -> Value {
parse_date_into_table(Ok(n), head)
}
Value::Date { val, span: _ } => parse_date_into_table(Ok(val), head),
_ => unsupported_input_error(head),
_ => Value::Error {
error: DatetimeParseError(head),
},
}
}

View file

@ -1,5 +1,5 @@
use super::parser::datetime_in_timezone;
use crate::date::utils::{parse_date_from_string, unsupported_input_error};
use crate::date::utils::parse_date_from_string;
use chrono::{DateTime, Local};
use nu_engine::CallExt;
use nu_protocol::ast::Call;
@ -93,7 +93,7 @@ fn helper(value: Value, head: Span, timezone: &Spanned<String>) -> Value {
val,
span: val_span,
} => {
let time = parse_date_from_string(val, val_span);
let time = parse_date_from_string(&val, val_span);
match time {
Ok(dt) => _to_timezone(dt, timezone, head),
Err(e) => e,
@ -104,7 +104,9 @@ fn helper(value: Value, head: Span, timezone: &Spanned<String>) -> Value {
let dt = Local::now();
_to_timezone(dt.with_timezone(dt.offset()), timezone, head)
}
_ => unsupported_input_error(head),
_ => Value::Error {
error: ShellError::DatetimeParseError(head),
},
}
}

View file

@ -1,43 +1,26 @@
use chrono::{DateTime, FixedOffset};
use chrono::{DateTime, FixedOffset, Local, LocalResult, TimeZone};
use nu_protocol::{ShellError, Span, Value};
pub fn unsupported_input_error(span: Span) -> Value {
Value::Error {
error: ShellError::UnsupportedInput(
String::from(
"Only dates with timezones are supported. The following formats are allowed \n
* %Y-%m-%d %H:%M:%S %z -- 2020-04-12 22:10:57 +02:00 \n
* %Y-%m-%d %H:%M:%S%.6f %z -- 2020-04-12 22:10:57.213231 +02:00 \n
* rfc3339 -- 2020-04-12T22:10:57+02:00 \n
* rfc2822 -- Tue, 1 Jul 2003 10:52:37 +0200",
),
span,
),
}
}
pub fn parse_date_from_string(input: String, span: Span) -> Result<DateTime<FixedOffset>, Value> {
let datetime = DateTime::parse_from_str(&input, "%Y-%m-%d %H:%M:%S %z"); // "2020-04-12 22:10:57 +02:00";
match datetime {
Ok(x) => Ok(x),
Err(_) => {
let datetime = DateTime::parse_from_str(&input, "%Y-%m-%d %H:%M:%S%.6f %z"); // "2020-04-12 22:10:57.213231 +02:00";
match datetime {
Ok(x) => Ok(x),
Err(_) => {
let datetime = DateTime::parse_from_rfc3339(&input); // "2020-04-12T22:10:57+02:00";
match datetime {
Ok(x) => Ok(x),
Err(_) => {
let datetime = DateTime::parse_from_rfc2822(&input); // "Tue, 1 Jul 2003 10:52:37 +0200";
match datetime {
Ok(x) => Ok(x),
Err(_) => Err(unsupported_input_error(span)),
}
}
}
}
pub(crate) fn parse_date_from_string(
input: &str,
span: Span,
) -> Result<DateTime<FixedOffset>, Value> {
match dtparse::parse(input) {
Ok((native_dt, fixed_offset)) => {
let offset = match fixed_offset {
Some(fo) => fo,
None => *(Local::now().offset()),
};
match offset.from_local_datetime(&native_dt) {
LocalResult::Single(d) => Ok(d),
LocalResult::Ambiguous(d, _) => Ok(d),
LocalResult::None => Err(Value::Error {
error: ShellError::DatetimeParseError(span),
}),
}
}
Err(_) => Err(Value::Error {
error: ShellError::DatetimeParseError(span),
}),
}
}

View file

@ -154,6 +154,22 @@ pub enum ShellError {
#[diagnostic(code(nu::shell::unsupported_input), url(docsrs))]
UnsupportedInput(String, #[label("{0} not supported")] Span),
#[error("Unable to parse datetime")]
#[diagnostic(
code(nu::shell::datetime_parse_error),
url(docsrs),
help(
r#"Examples of supported inputs:
* "5 pm"
* "2020/12/4"
* "2020.12.04 22:10 +2"
* "2020-04-12 22:10:57 +02:00"
* "2020-04-12T22:10:57.213231+02:00"
* "Tue, 1 Jul 2003 10:52:37 +0200""#
)
)]
DatetimeParseError(#[label("datetime parsing failed")] Span),
#[error("Network failure")]
#[diagnostic(code(nu::shell::network_failure), url(docsrs))]
NetworkFailure(String, #[label("{0}")] Span),