From 39edd7e08045c65ec9be6848321cf8ea291e59af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Riegel?= <96702577+LoicRiegel@users.noreply.github.com> Date: Fri, 11 Apr 2025 14:48:39 +0200 Subject: [PATCH] Bugfix: datetime parsing and local timezones (#15544) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hi, This PR should close 3 issues - [DMY date format is parsed inconsistently #14123](https://github.com/nushell/nushell/issues/14123) - [into datetime doesnt't work with --format and ignores user's locale #11015](https://github.com/nushell/nushell/issues/11015) - [into datetime: iinconsistent and incrrect behaviour regarding timezones #13823](https://github.com/nushell/nushell/issues/13823) # Description - Allow to parse only dates or only times with --format - Use local timezone depending on the input. Ex: I'm in France, so show dates with +0100 in winter and +0200 in summer. ```nushell # Concerning #13823 > "2020-01-01 12:00" | into datetime Wed, 1 Jan 2020 12:00:00 +0100 (5 years ago) # OK, it's my timezone in winter time > "2020-06-01 12:00" | into datetime Mon, 1 Jun 2020 12:00:00 +0200 (4 years ago) # OK, it's my timezone in summertime > ("2024-10-27 12:00" | into datetime) - ("2024-10-27 00:00" | into datetime) 13hr # Ok, because we switched from summer to winter time on 2025-10-27, so there are actually 13h between midnight and noon > "2020-01-01 12:00" | into datetime --format "%Y-%m-%d %H:%M" Wed, 1 Jan 2020 12:00:00 +0100 (5 years ago) # OK: timezone is assumed to be local, and +0100 is my timezone in winter # Concerning #14123 and #11015 # Flexible parsing still works like before, which could be counter-intuitive, but it's flexible parsing # with one difference: the timezone is local > '12-01-2001' | into datetime Sat, 1 Dec 2001 00:00:00 +0100 (23 years ago) # OK, +0100 is my timezone in winter time. If I run it with nushell 0.103.0 in summer time, I get +0200 > '13-01-2001' | into datetime Sat, 13 Jan 2001 00:00:00 +0100 (24 years ago) ## If you want, you can use the --format option to parse a date or a time (before, it had to be a date + time) ## Notice here again the timezone is correct depending on winter/summer time ~> "06.03.2023" | into datetime -f "%d.%m.%Y" Mon, 6 Mar 2023 00:00:00 +0100 (2 years ago) ~> "06.03.2023" | into datetime -f "%m.%d.%Y" Sat, 3 Jun 2023 00:00:00 +0200 (2 years ago) > "10:00" | into datetime --format "%H:%M" Thu, 10 Apr 2025 10:00:00 +0200 (9 hours ago) ``` # User-Facing Changes See above # Tests + Formatting # After Submitting I'll down something for the release notes, if this is merged in time 😄 --- .../src/conversions/into/datetime.rs | 37 +++++++++++++++---- crates/nu-command/src/date/utils.rs | 6 ++- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/crates/nu-command/src/conversions/into/datetime.rs b/crates/nu-command/src/conversions/into/datetime.rs index c75b7ad677..0eff739d5f 100644 --- a/crates/nu-command/src/conversions/into/datetime.rs +++ b/crates/nu-command/src/conversions/into/datetime.rs @@ -458,13 +458,8 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { } }, Err(reason) => { - match NaiveDateTime::parse_from_str(val, &dt_format.item.0) { - Ok(d) => { - let dt_fixed = - Local.from_local_datetime(&d).single().unwrap_or_default(); - - Value::date(dt_fixed.into(),head) - } + match parse_with_format(val, &dt_format.item.0, head) { + Ok(parsed) => parsed, Err(_) => { Value::error ( ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt_format.item.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()) }, @@ -808,6 +803,34 @@ fn parse_timezone_from_record( } } +fn parse_with_format(val: &str, fmt: &str, head: Span) -> Result { + // try parsing at date + time + if let Ok(dt) = NaiveDateTime::parse_from_str(val, fmt) { + let dt_native = Local.from_local_datetime(&dt).single().unwrap_or_default(); + return Ok(Value::date(dt_native.into(), head)); + } + + // try parsing at date only + if let Ok(date) = NaiveDate::parse_from_str(val, fmt) { + if let Some(dt) = date.and_hms_opt(0, 0, 0) { + let dt_native = Local.from_local_datetime(&dt).single().unwrap_or_default(); + return Ok(Value::date(dt_native.into(), head)); + } + } + + // try parsing at time only + if let Ok(time) = NaiveTime::parse_from_str(val, fmt) { + let now = Local::now().naive_local().date(); + let dt_native = Local + .from_local_datetime(&now.and_time(time)) + .single() + .unwrap_or_default(); + return Ok(Value::date(dt_native.into(), head)); + } + + Err(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/nu-command/src/date/utils.rs b/crates/nu-command/src/date/utils.rs index 4616dc887b..62e8c17be8 100644 --- a/crates/nu-command/src/date/utils.rs +++ b/crates/nu-command/src/date/utils.rs @@ -9,7 +9,11 @@ pub(crate) fn parse_date_from_string( Ok((native_dt, fixed_offset)) => { let offset = match fixed_offset { Some(offset) => offset, - None => *(Local::now().offset()), + None => *Local + .from_local_datetime(&native_dt) + .single() + .unwrap_or_default() + .offset(), }; match offset.from_local_datetime(&native_dt) { LocalResult::Single(d) => Ok(d),