Bugfix: datetime parsing and local timezones (#15544)

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 😄
This commit is contained in:
Loïc Riegel 2025-04-11 14:48:39 +02:00 committed by GitHub
parent 61dbcf3de6
commit 39edd7e080
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 35 additions and 8 deletions

View File

@ -458,13 +458,8 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
} }
}, },
Err(reason) => { Err(reason) => {
match NaiveDateTime::parse_from_str(val, &dt_format.item.0) { match parse_with_format(val, &dt_format.item.0, head) {
Ok(d) => { Ok(parsed) => parsed,
let dt_fixed =
Local.from_local_datetime(&d).single().unwrap_or_default();
Value::date(dt_fixed.into(),head)
}
Err(_) => { Err(_) => {
Value::error ( 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()) }, 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<Value, ()> {
// 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -9,7 +9,11 @@ pub(crate) fn parse_date_from_string(
Ok((native_dt, fixed_offset)) => { Ok((native_dt, fixed_offset)) => {
let offset = match fixed_offset { let offset = match fixed_offset {
Some(offset) => 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) { match offset.from_local_datetime(&native_dt) {
LocalResult::Single(d) => Ok(d), LocalResult::Single(d) => Ok(d),