From 4e83ccdf86c71811f54919ab0eb4c2d38eba4005 Mon Sep 17 00:00:00 2001 From: NotTheDr01ds <32344964+NotTheDr01ds@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:05:32 -0400 Subject: [PATCH] Allow `int` input when using a formatstring in `into datetime` (#13541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description When using a format string, `into datetime` would disallow an `int` even when it logically made sense. This was mainly a problem when attempting to convert a Unix epoch to Nushell `datetime`. Unix epochs are often stored or returned as `int` in external data sources. ```nu 1722821463 | into datetime -f '%s' Error: nu::shell::only_supports_this_input_type × Input type not supported. ╭─[entry #3:1:1] 1 │ 1722821463 | into datetime -f '%s' · ─────┬──── ──────┬────── · │ ╰── only string input data is supported · ╰── input type: int ╰──── ``` While the solution was simply to `| to text` the `int`, this PR handles the use-case automatically. Essentially a ~5 line change that just moves the current parsing to a closure that is called for both Strings and Ints-converted-to-Strings. # User-Facing Changes After the change: ```nu [ 1722821463 "1722821463" 0 ] | each { into datetime -f '%s' } ╭───┬──────────────╮ │ 0 │ 10 hours ago │ │ 1 │ 10 hours ago │ │ 2 │ 54 years ago │ ╰───┴──────────────╯ ``` # Tests + Formatting Test case added. - :green_circle: `toolkit fmt` - :green_circle: `toolkit clippy` - :green_circle: `toolkit test` - :green_circle: `toolkit test stdlib` # After Submitting --- .../src/conversions/into/datetime.rs | 85 ++++++++++++------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/crates/nu-command/src/conversions/into/datetime.rs b/crates/nu-command/src/conversions/into/datetime.rs index 8dc0340ba1..9dd6ad9335 100644 --- a/crates/nu-command/src/conversions/into/datetime.rs +++ b/crates/nu-command/src/conversions/into/datetime.rs @@ -379,42 +379,47 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { // If input is not a timestamp, try parsing it as a string let span = input.span(); - match input { - Value::String { val, .. } => { - match dateformat { - Some(dt) => match DateTime::parse_from_str(val, &dt.0) { - Ok(d) => Value::date ( d, head ), - Err(reason) => { - match NaiveDateTime::parse_from_str(val, &dt.0) { - Ok(d) => Value::date ( - DateTime::from_naive_utc_and_offset( - d, - *Local::now().offset(), - ), - head, + + let parse_as_string = |val: &str| { + match dateformat { + Some(dt) => match DateTime::parse_from_str(val, &dt.0) { + Ok(d) => Value::date ( d, head ), + Err(reason) => { + match NaiveDateTime::parse_from_str(val, &dt.0) { + Ok(d) => Value::date ( + DateTime::from_naive_utc_and_offset( + d, + *Local::now().offset(), ), - Err(_) => { - Value::error ( - ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt.0), from_type: reason.to_string(), span: head, help: Some("you can use `into datetime` without a format string to enable flexible parsing".to_string()) }, - head, - ) - } + head, + ), + Err(_) => { + Value::error ( + ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt.0), from_type: reason.to_string(), span: head, help: Some("you can use `into datetime` without a format string to enable flexible parsing".to_string()) }, + head, + ) } } - }, + } + }, - // 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(val, span) { - Ok(date) => Value::date ( - date, - span, - ), - Err(err) => err, - }, - } + // 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(val, span) { + Ok(date) => Value::date ( + date, + span, + ), + Err(err) => err, + }, } + }; + + match input { + Value::String { val, .. } => parse_as_string(val), + Value::Int { val, .. } => parse_as_string(&val.to_string()), + // Propagate errors by explicitly matching them before the final case. Value::Error { .. } => input.clone(), other => Value::error( @@ -575,6 +580,24 @@ mod tests { assert_eq!(actual, expected) } + #[test] + fn takes_int_with_formatstring() { + let date_int = Value::test_int(1_614_434_140); + let fmt_options = Some(DatetimeFormat("%s".to_string())); + let args = Arguments { + zone_options: None, + format_options: fmt_options, + cell_paths: None, + }; + let actual = action(&date_int, &args, Span::test_data()); + let expected = Value::date( + DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(), + Span::test_data(), + ); + + assert_eq!(actual, expected) + } + #[test] fn takes_timestamp() { let date_str = Value::test_string("1614434140000000000");