From 4367aa9f58cbcc1f97e2b1409e9af0d2ccf6a3e5 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Wed, 15 Nov 2023 17:43:37 -0600 Subject: [PATCH] allow parsing of human readable datetimes (#11051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description This PR adds the ability to parse human readable datetime strings as part of the `into datetime` command. I added a new `-n`/`--list-human` parameter that produces this list to give the user an idea of what is supported. ```nushell ❯ into datetime --list-human ╭#─┬parseable human datetime examples┬───result───╮ │0 │Today 18:30 │in 8 hours │ │1 │2022-11-07 13:25:30 │a year ago │ │2 │15:20 Friday │in 3 days │ │3 │This Friday 17:00 │in 3 days │ │4 │13:25, Next Tuesday │in a week │ │5 │Last Friday at 19:45 │3 days ago │ │6 │In 3 days │in 2 days │ │7 │In 2 hours │in 2 hours │ │8 │10 hours and 5 minutes ago │10 hours ago│ │9 │1 years ago │a year ago │ │10│A year ago │a year ago │ │11│A month ago │a month ago │ │12│A week ago │a week ago │ │13│A day ago │a day ago │ │14│An hour ago │an hour ago │ │15│A minute ago │a minute ago│ │16│A second ago │now │ │17│Now │now │ ╰#─┴parseable human datetime examples┴───result───╯ ``` Or with `$env.config.datetime_format.table` set. ```nushell ❯ into datetime --list-human ╭#─┬parseable human datetime examples┬──────result───────╮ │0 │Today 18:30 │11/14/23 06:30:00PM│ │1 │2022-11-07 13:25:30 │11/07/22 01:25:30PM│ │2 │15:20 Friday │11/17/23 03:20:00PM│ │3 │This Friday 17:00 │11/17/23 05:00:00PM│ │4 │13:25, Next Tuesday │11/21/23 01:25:00PM│ │5 │Last Friday at 19:45 │11/10/23 07:45:00PM│ │6 │In 3 days │11/17/23 10:12:54AM│ │7 │In 2 hours │11/14/23 12:12:54PM│ │8 │10 hours and 5 minutes ago │11/14/23 12:07:54AM│ │9 │1 years ago │11/13/22 10:12:54AM│ │10│A year ago │11/13/22 10:12:54AM│ │11│A month ago │10/15/23 11:12:54AM│ │12│A week ago │11/07/23 10:12:54AM│ │13│A day ago │11/13/23 10:12:54AM│ │14│An hour ago │11/14/23 09:12:54AM│ │15│A minute ago │11/14/23 10:11:54AM│ │16│A second ago │11/14/23 10:12:53AM│ │17│Now │11/14/23 10:12:54AM│ ╰#─┴parseable human datetime examples┴──────result───────╯ ``` # User-Facing Changes # Tests + Formatting # After Submitting --- Cargo.lock | 66 +++++++++++++ crates/nu-command/Cargo.toml | 1 + .../src/conversions/into/datetime.rs | 99 +++++++++++++++++-- 3 files changed, 160 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc9f2a193b..15834204c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -585,9 +585,11 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "pure-rust-locales", "serde", + "wasm-bindgen", "windows-targets 0.48.5", ] @@ -1778,6 +1780,18 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "human-date-parser" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d65b3ad1fdc03306397b6004b4f8f765cf7467194a1080b4530eeed5a2f0bc" +dependencies = [ + "chrono", + "pest", + "pest_derive", + "thiserror", +] + [[package]] name = "hyper" version = "0.14.27" @@ -2845,6 +2859,7 @@ dependencies = [ "filetime", "fs_extra", "htmlescape", + "human-date-parser", "indexmap 2.1.0", "indicatif", "itertools 0.11.0", @@ -3593,6 +3608,51 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f658886ed52e196e850cfbbfddab9eaa7f6d90dd0929e264c31e5cec07e09e57" +[[package]] +name = "pest" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "pest_meta" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "phf" version = "0.10.1" @@ -5568,6 +5628,12 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "umask" version = "2.1.0" diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 45eaba3853..d30698832b 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -47,6 +47,7 @@ filesize = "0.2" filetime = "0.2" fs_extra = "1.3" htmlescape = "0.3" +human-date-parser = "0.1.1" indexmap = "2.1" indicatif = "0.17" itertools = "0.11" diff --git a/crates/nu-command/src/conversions/into/datetime.rs b/crates/nu-command/src/conversions/into/datetime.rs index 6272be93ec..5ac6a7bc84 100644 --- a/crates/nu-command/src/conversions/into/datetime.rs +++ b/crates/nu-command/src/conversions/into/datetime.rs @@ -1,13 +1,14 @@ use crate::{generate_strftime_list, parse_date_from_string}; +use chrono::NaiveTime; use chrono::{DateTime, FixedOffset, Local, TimeZone, Utc}; +use human_date_parser::{from_human_time, ParseResult}; use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::CallExt; -use nu_protocol::ast::Call; -use nu_protocol::ast::CellPath; -use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ - Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Spanned, - SyntaxShape, Type, Value, + ast::{Call, CellPath}, + engine::{Command, EngineState, Stack}, + record, Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, + Spanned, SyntaxShape, Type, Value, }; struct Arguments { @@ -95,6 +96,11 @@ impl Command for SubCommand { "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, @@ -112,6 +118,8 @@ impl Command for SubCommand { ) -> Result { if call.has_flag("list") { Ok(generate_strftime_list(call.head, true).into_pipeline_data()) + } else if call.has_flag("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); @@ -225,6 +233,21 @@ impl Command for SubCommand { 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, + }, ] } } @@ -241,7 +264,33 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { if let Ok(input_val) = input.as_spanned_string() { match parse_date_from_string(&input_val.item, input_val.span) { Ok(date) => return Value::date(date, input_val.span), - Err(err) => err, + Err(_) => { + if let Ok(date) = from_human_time(&input_val.item) { + match date { + ParseResult::Date(date) => { + let time = NaiveTime::from_hms_opt(0, 0, 0).expect("valid time"); + let combined = date.and_time(time); + let dt_fixed = DateTime::from_naive_utc_and_offset( + combined, + *Local::now().offset(), + ); + return Value::date(dt_fixed, input_val.span); + } + ParseResult::DateTime(date) => { + return Value::date(date.fixed_offset(), input_val.span) + } + ParseResult::Time(time) => { + let date = Local::now().date_naive(); + let combined = date.and_time(time); + let dt_fixed = DateTime::from_naive_utc_and_offset( + combined, + *Local::now().offset(), + ); + return Value::date(dt_fixed, input_val.span); + } + } + } + } }; } } @@ -362,6 +411,44 @@ 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::*;