Bugfix chrono panic + hotifx PR15544 (#15549)

Closes  #13972

# Description
First commit: a hotfix concerning my last PR #15544! I had a
``unwrap_or_default`` that resulted in all years before ~1800 being
considered as "now", because the ``num_nanoseconds()`` overflowed.
Cc @fdncred 

Second: about #13972
Negative years are not allowed with RFC 2822 formatting, so I fallback
RTC 3339 in such cases.

If you want you might Rebase and Merge, and not squash.

# User-Facing Changes
On master 🔴 :
```nu
~> {year: 1900} | into datetime
Mon, 1 Jan 1900 00:00:00 +0200 (125 years ago)
# OK

~> {year: 1000} | into datetime
Wed, 1 Jan 1000 00:00:00 +0200 (now)
# NOT OK: now?

~> {year: -1000} | into datetime
-1000-01-01T00:00:00+02:00 (now)
# NOT OK: now?

~> {year: -1000} | into datetime | format date 
Error:   × Main thread panicked.
  ├─▶ at C:\Users\RIL1RT\.cargo\registry\src\index.crates.io-6f17d22bba15001f\chrono-0.4.39\src\datetime\mod.rs:626:14
  ╰─▶ writing rfc2822 datetime to string should never fail: Error
  help: set the `RUST_BACKTRACE=1` environment variable to display a backtrace.
# NOT OK: panics
```

On this branch 🟢 :
```nu
~> {year: 1900} | into datetime
Mon, 1 Jan 1900 00:00:00 +0200 (in 125 years)
~>  {year: 1000} | into datetime
Wed, 1 Jan 1000 00:00:00 +0200 (1025 years ago)
~> {year: -1000} | into datetime
-1000-01-01T00:00:00+02:00 (3025 years ago)
~> {year: -1000} | into datetime | format date
-1000-01-01T00:00:00+02:00
~> '3000 years ago' | date from-human | format date
-0975-04-11T18:18:24.301641100+02:00
```

# Tests + Formatting

# After Submitting
Nothing required IMO
This commit is contained in:
Loïc Riegel 2025-04-11 18:52:42 +02:00 committed by GitHub
parent 1a0778d77e
commit c8c018452f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 56 additions and 8 deletions

View File

@ -1,3 +1,4 @@
use chrono::Datelike;
use chrono_humanize::HumanTime;
use nu_engine::command_prelude::*;
use nu_protocol::{format_duration, shell_error::io::IoError, ByteStream, PipelineMetadata};
@ -167,7 +168,17 @@ fn local_into_string(
Value::Filesize { val, .. } => val.to_string(),
Value::Duration { val, .. } => format_duration(val),
Value::Date { val, .. } => {
format!("{} ({})", val.to_rfc2822(), HumanTime::from(val))
format!(
"{} ({})",
{
if val.year() >= 0 {
val.to_rfc2822()
} else {
val.to_rfc3339()
}
},
HumanTime::from(val)
)
}
Value::Range { val, .. } => val.to_string(),
Value::String { val, .. } => val,

View File

@ -1,5 +1,5 @@
use crate::{generate_strftime_list, parse_date_from_string};
use chrono::{DateTime, Locale, TimeZone};
use chrono::{DateTime, Datelike, Locale, TimeZone};
use nu_engine::command_prelude::*;
use nu_utils::locale::{get_system_locale_string, LOCALE_OVERRIDE_ENV_VAR};
@ -228,11 +228,29 @@ fn format_helper(
fn format_helper_rfc2822(value: Value, span: Span) -> Value {
let val_span = value.span();
match value {
Value::Date { val, .. } => Value::string(val.to_rfc2822(), span),
Value::Date { val, .. } => Value::string(
{
if val.year() >= 0 {
val.to_rfc2822()
} else {
val.to_rfc3339()
}
},
span,
),
Value::String { val, .. } => {
let dt = parse_date_from_string(&val, val_span);
match dt {
Ok(x) => Value::string(x.to_rfc2822(), span),
Ok(x) => Value::string(
{
if x.year() >= 0 {
x.to_rfc2822()
} else {
x.to_rfc3339()
}
},
span,
),
Err(e) => e,
}
}

View File

@ -14,6 +14,16 @@ fn into_datetime_from_record() {
assert_eq!(expected.out, actual.out);
}
#[test]
fn into_datetime_from_record_very_old() {
let actual = nu!(r#"{year: -100, timezone: '+02:00'} | into datetime | into record"#);
let expected = nu!(
r#"{year: -100, month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0, timezone: '+02:00'}"#
);
assert_eq!(expected.out, actual.out);
}
#[test]
fn into_datetime_from_record_defaults() {
let actual = nu!(r#"{year: 2025, timezone: '+02:00'} | into datetime | into record"#);

View File

@ -4020,10 +4020,19 @@ fn operator_type_error(
fn human_time_from_now(val: &DateTime<FixedOffset>) -> HumanTime {
let now = Local::now().with_timezone(val.offset());
let delta = *val - now;
let delta_seconds = delta.num_nanoseconds().unwrap_or(0) as f64 / 1_000_000_000.0;
let delta_seconds_rounded = delta_seconds.round() as i64;
HumanTime::from(Duration::seconds(delta_seconds_rounded))
match delta.num_nanoseconds() {
Some(num_nanoseconds) => {
let delta_seconds = num_nanoseconds as f64 / 1_000_000_000.0;
let delta_seconds_rounded = delta_seconds.round() as i64;
HumanTime::from(Duration::seconds(delta_seconds_rounded))
}
None => {
// Happens if the total number of nanoseconds exceeds what fits in an i64
// Note: not using delta.num_days() because it results is wrong for years before ~936: a extra year is added
let delta_years = val.year() - now.year();
HumanTime::from(Duration::days(delta_years as i64 * 365))
}
}
}
#[cfg(test)]