diff --git a/crates/nu-command/src/conversions/into/datetime.rs b/crates/nu-command/src/conversions/into/datetime.rs index 1ff2d55522..e503f32653 100644 --- a/crates/nu-command/src/conversions/into/datetime.rs +++ b/crates/nu-command/src/conversions/into/datetime.rs @@ -1,6 +1,5 @@ use crate::{generate_strftime_list, parse_date_from_string}; use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, TimeZone, Utc}; -use human_date_parser::{from_human_time, ParseResult}; use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; @@ -98,11 +97,6 @@ impl Command for IntoDatetime { "Show all possible variables for use in --format flag", Some('l'), ) - .switch( - "list-human", - "Show human-readable datetime parsing examples", - Some('n'), - ) .rest( "rest", SyntaxShape::CellPath, @@ -120,8 +114,6 @@ impl Command for IntoDatetime { ) -> Result { if call.has_flag(engine_state, stack, "list")? { Ok(generate_strftime_list(call.head, true).into_pipeline_data()) - } else if call.has_flag(engine_state, stack, "list-human")? { - Ok(list_human_readable_examples(call.head).into_pipeline_data()) } else { let cell_paths = call.rest(engine_state, stack, 0)?; let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); @@ -256,21 +248,6 @@ impl Command for IntoDatetime { Span::test_data(), )), }, - Example { - description: "Parsing human readable datetimes", - example: "'Today at 18:30' | into datetime", - result: None, - }, - Example { - description: "Parsing human readable datetimes", - example: "'Last Friday at 19:45' | into datetime", - result: None, - }, - Example { - description: "Parsing human readable datetimes", - example: "'In 5 minutes and 30 seconds' | into datetime", - result: None, - }, ] } } @@ -291,60 +268,9 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { if matches!(input, Value::String { .. }) && dateformat.is_none() { let span = input.span(); if let Ok(input_val) = input.coerce_str() { - match parse_date_from_string(&input_val, span) { - Ok(date) => return Value::date(date, span), - Err(_) => { - if let Ok(date) = from_human_time(&input_val, Local::now().naive_local()) { - match date { - ParseResult::Date(date) => { - let time = Local::now().time(); - let combined = date.and_time(time); - let local_offset = *Local::now().offset(); - let dt_fixed = - TimeZone::from_local_datetime(&local_offset, &combined) - .single() - .unwrap_or_default(); - return Value::date(dt_fixed, span); - } - ParseResult::DateTime(date) => { - let local_offset = *Local::now().offset(); - let dt_fixed = match local_offset.from_local_datetime(&date) { - chrono::LocalResult::Single(dt) => dt, - chrono::LocalResult::Ambiguous(_, _) => { - return Value::error( - ShellError::DatetimeParseError { - msg: "Ambiguous datetime".to_string(), - span, - }, - span, - ); - } - chrono::LocalResult::None => { - return Value::error( - ShellError::DatetimeParseError { - msg: "Invalid datetime".to_string(), - span, - }, - span, - ); - } - }; - return Value::date(dt_fixed, span); - } - ParseResult::Time(time) => { - let date = Local::now().date_naive(); - let combined = date.and_time(time); - let local_offset = *Local::now().offset(); - let dt_fixed = - TimeZone::from_local_datetime(&local_offset, &combined) - .single() - .unwrap_or_default(); - return Value::date(dt_fixed, span); - } - } - } - } - }; + if let Ok(date) = parse_date_from_string(&input_val, span) { + return Value::date(date, span); + } } } @@ -524,44 +450,6 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { } } -fn list_human_readable_examples(span: Span) -> Value { - let examples: Vec = vec![ - "Today 18:30".into(), - "2022-11-07 13:25:30".into(), - "15:20 Friday".into(), - "This Friday 17:00".into(), - "13:25, Next Tuesday".into(), - "Last Friday at 19:45".into(), - "In 3 days".into(), - "In 2 hours".into(), - "10 hours and 5 minutes ago".into(), - "1 years ago".into(), - "A year ago".into(), - "A month ago".into(), - "A week ago".into(), - "A day ago".into(), - "An hour ago".into(), - "A minute ago".into(), - "A second ago".into(), - "Now".into(), - ]; - - let records = examples - .iter() - .map(|s| { - Value::record( - record! { - "parseable human datetime examples" => Value::test_string(s.to_string()), - "result" => action(&Value::test_string(s.to_string()), &Arguments { zone_options: None, format_options: None, cell_paths: None }, span) - }, - span, - ) - }) - .collect::>(); - - Value::list(records, span) -} - #[cfg(test)] mod tests { use super::*; @@ -593,14 +481,7 @@ mod tests { } #[test] - #[ignore] fn takes_a_date_format_without_timezone() { - // Ignoring this test for now because we changed the human-date-parser to use - // the users timezone instead of UTC. We may continue to tweak this behavior. - // Another hacky solution is to set the timezone to UTC in the test, which works - // on MacOS and Linux but hasn't been tested on Windows. Plus it kind of defeats - // the purpose of a "without_timezone" test. - // std::env::set_var("TZ", "UTC"); let date_str = Value::test_string("16.11.1984 8:00 am"); let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P".to_string())); let args = Arguments { diff --git a/crates/nu-command/src/date/from_human.rs b/crates/nu-command/src/date/from_human.rs new file mode 100644 index 0000000000..f924b4d6d5 --- /dev/null +++ b/crates/nu-command/src/date/from_human.rs @@ -0,0 +1,259 @@ +use chrono::{Local, TimeZone}; +use human_date_parser::{from_human_time, ParseResult}; +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct DateFromHuman; + +impl Command for DateFromHuman { + fn name(&self) -> &str { + "date from-human" + } + + fn signature(&self) -> Signature { + Signature::build("date from-human") + .input_output_types(vec![ + (Type::String, Type::Date), + (Type::Nothing, Type::table()), + ]) + .allow_variants_without_examples(true) + .switch( + "list", + "Show human-readable datetime parsing examples", + Some('l'), + ) + .category(Category::Date) + } + + fn description(&self) -> &str { + "Convert a human readable datetime string to a datetime." + } + + fn search_terms(&self) -> Vec<&str> { + vec![ + "relative", + "now", + "today", + "tomorrow", + "yesterday", + "weekday", + "weekday_name", + "timezone", + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + if call.has_flag(engine_state, stack, "list")? { + return Ok(list_human_readable_examples(call.head).into_pipeline_data()); + } + let head = call.head; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map(move |value| helper(value, head), engine_state.signals()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Parsing human readable datetime", + example: "'Today at 18:30' | date from-human", + result: None, + }, + Example { + description: "Parsing human readable datetime", + example: "'Last Friday at 19:45' | date from-human", + result: None, + }, + Example { + description: "Parsing human readable datetime", + example: "'In 5 minutes and 30 seconds' | date from-human", + result: None, + }, + Example { + description: "PShow human-readable datetime parsing examples", + example: "date from-human --list", + result: None, + }, + ] + } +} + +fn helper(value: Value, head: Span) -> Value { + let span = value.span(); + let input_val = match value { + Value::String { val, .. } => val, + other => { + return Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "string".to_string(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: span, + }, + span, + ) + } + }; + + if let Ok(date) = from_human_time(&input_val, Local::now().naive_local()) { + match date { + ParseResult::Date(date) => { + let time = Local::now().time(); + let combined = date.and_time(time); + let local_offset = *Local::now().offset(); + let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined) + .single() + .unwrap_or_default(); + return Value::date(dt_fixed, span); + } + ParseResult::DateTime(date) => { + let local_offset = *Local::now().offset(); + let dt_fixed = match local_offset.from_local_datetime(&date) { + chrono::LocalResult::Single(dt) => dt, + chrono::LocalResult::Ambiguous(_, _) => { + return Value::error( + ShellError::DatetimeParseError { + msg: "Ambiguous datetime".to_string(), + span, + }, + span, + ); + } + chrono::LocalResult::None => { + return Value::error( + ShellError::DatetimeParseError { + msg: "Invalid datetime".to_string(), + span, + }, + span, + ); + } + }; + return Value::date(dt_fixed, span); + } + ParseResult::Time(time) => { + let date = Local::now().date_naive(); + let combined = date.and_time(time); + let local_offset = *Local::now().offset(); + let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined) + .single() + .unwrap_or_default(); + return Value::date(dt_fixed, span); + } + } + } + + match from_human_time(&input_val, Local::now().naive_local()) { + Ok(date) => match date { + ParseResult::Date(date) => { + let time = Local::now().time(); + let combined = date.and_time(time); + let local_offset = *Local::now().offset(); + let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined) + .single() + .unwrap_or_default(); + Value::date(dt_fixed, span) + } + ParseResult::DateTime(date) => { + let local_offset = *Local::now().offset(); + let dt_fixed = match local_offset.from_local_datetime(&date) { + chrono::LocalResult::Single(dt) => dt, + chrono::LocalResult::Ambiguous(_, _) => { + return Value::error( + ShellError::DatetimeParseError { + msg: "Ambiguous datetime".to_string(), + span, + }, + span, + ); + } + chrono::LocalResult::None => { + return Value::error( + ShellError::DatetimeParseError { + msg: "Invalid datetime".to_string(), + span, + }, + span, + ); + } + }; + Value::date(dt_fixed, span) + } + ParseResult::Time(time) => { + let date = Local::now().date_naive(); + let combined = date.and_time(time); + let local_offset = *Local::now().offset(); + let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined) + .single() + .unwrap_or_default(); + Value::date(dt_fixed, span) + } + }, + Err(_) => Value::error( + ShellError::IncorrectValue { + msg: "Cannot parse as humanized date".to_string(), + val_span: head, + call_span: span, + }, + span, + ), + } +} + +fn list_human_readable_examples(span: Span) -> Value { + let examples: Vec = vec![ + "Today 18:30".into(), + "2022-11-07 13:25:30".into(), + "15:20 Friday".into(), + "This Friday 17:00".into(), + "13:25, Next Tuesday".into(), + "Last Friday at 19:45".into(), + "In 3 days".into(), + "In 2 hours".into(), + "10 hours and 5 minutes ago".into(), + "1 years ago".into(), + "A year ago".into(), + "A month ago".into(), + "A week ago".into(), + "A day ago".into(), + "An hour ago".into(), + "A minute ago".into(), + "A second ago".into(), + "Now".into(), + ]; + + let records = examples + .iter() + .map(|s| { + Value::record( + record! { + "parseable human datetime examples" => Value::test_string(s.to_string()), + "result" => helper(Value::test_string(s.to_string()), span), + }, + span, + ) + }) + .collect::>(); + + Value::list(records, span) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(DateFromHuman {}) + } +} diff --git a/crates/nu-command/src/date/mod.rs b/crates/nu-command/src/date/mod.rs index 385420c911..e95acef3dd 100644 --- a/crates/nu-command/src/date/mod.rs +++ b/crates/nu-command/src/date/mod.rs @@ -1,4 +1,5 @@ mod date_; +mod from_human; mod humanize; mod list_timezone; mod now; @@ -7,6 +8,7 @@ mod to_timezone; mod utils; pub use date_::Date; +pub use from_human::DateFromHuman; pub use humanize::DateHumanize; pub use list_timezone::DateListTimezones; pub use now::DateNow; diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 0c1b38182a..d4cd7e22f5 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -272,6 +272,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { // Date bind_command! { Date, + DateFromHuman, DateHumanize, DateListTimezones, DateNow,