Bugfix/loss of precision when parsing value with unit (#15606)

Closes #12858

# Description
As explained in the ticket, easy to reproduce. Example: 1.07 minute is
1.07*60=64.2 secondes
```nushell
# before - wrong
> 1.07min
1min 4sec

# now - right
> 1.07min
1min 4sec 200ms
```

# User-Facing Changes
Bug is fixed when using ``into duration``.

# Tests + Formatting
Added a test for ``into duration``
Fixed ``parse_long_duration`` test: we gained precision 😄 

# After Submitting
Release notes? Or blog is enough? Let me know
This commit is contained in:
Loïc Riegel 2025-04-20 00:02:40 +02:00 committed by GitHub
parent 24dba9dc53
commit 1503ee09ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 52 additions and 12 deletions

View File

@ -0,0 +1,10 @@
use nu_test_support::nu;
// Tests happy paths
#[test]
fn into_duration_float() {
let actual = nu!(r#"1.07min | into duration"#);
assert_eq!("1min 4sec 200ms", actual.out);
}

View File

@ -375,7 +375,7 @@ fn duration_decimal_math_with_nanoseconds() {
"# "#
)); ));
assert_eq!(actual.out, "1wk 3day 10ns"); assert_eq!(actual.out, "1wk 3day 12hr 10ns");
} }
#[test] #[test]

View File

@ -54,6 +54,7 @@ mod insert;
mod inspect; mod inspect;
mod interleave; mod interleave;
mod into_datetime; mod into_datetime;
mod into_duration;
mod into_filesize; mod into_filesize;
mod into_int; mod into_int;
mod join; mod join;

View File

@ -2701,12 +2701,31 @@ pub fn parse_unit_value<'res>(
} }
}); });
let (num, unit) = match convert { let mut unit = match convert {
Some(convert_to) => ( Some(convert_to) => convert_to.0,
((number_part * convert_to.1 as f64) + (decimal_part * convert_to.1 as f64)) as i64, None => *unit,
convert_to.0, };
),
None => (number_part as i64, *unit), let num_float = match convert {
Some(convert_to) => {
(number_part * convert_to.1 as f64) + (decimal_part * convert_to.1 as f64)
}
None => number_part,
};
// Convert all durations to nanoseconds to not lose precision
let num = match unit_to_ns_factor(&unit) {
Some(factor) => {
let num_ns = num_float * factor;
if i64::MIN as f64 <= num_ns && num_ns <= i64::MAX as f64 {
unit = Unit::Nanosecond;
num_ns as i64
} else {
// not safe to convert, because of the overflow
num_float as i64
}
}
None => num_float as i64,
}; };
trace!("-- found {} {:?}", num, unit); trace!("-- found {} {:?}", num, unit);
@ -2813,6 +2832,20 @@ pub const DURATION_UNIT_GROUPS: &[UnitGroup] = &[
(Unit::Week, "wk", Some((Unit::Day, 7))), (Unit::Week, "wk", Some((Unit::Day, 7))),
]; ];
fn unit_to_ns_factor(unit: &Unit) -> Option<f64> {
match unit {
Unit::Nanosecond => Some(1.0),
Unit::Microsecond => Some(1_000.0),
Unit::Millisecond => Some(1_000_000.0),
Unit::Second => Some(1_000_000_000.0),
Unit::Minute => Some(60.0 * 1_000_000_000.0),
Unit::Hour => Some(60.0 * 60.0 * 1_000_000_000.0),
Unit::Day => Some(24.0 * 60.0 * 60.0 * 1_000_000_000.0),
Unit::Week => Some(7.0 * 24.0 * 60.0 * 60.0 * 1_000_000_000.0),
_ => None,
}
}
// Borrowed from libm at https://github.com/rust-lang/libm/blob/master/src/math/modf.rs // Borrowed from libm at https://github.com/rust-lang/libm/blob/master/src/math/modf.rs
fn modf(x: f64) -> (f64, f64) { fn modf(x: f64) -> (f64, f64) {
let rv2: f64; let rv2: f64;
@ -6878,13 +6911,9 @@ pub fn parse(
let mut output = { let mut output = {
if let Some(block) = previously_parsed_block { if let Some(block) = previously_parsed_block {
// dbg!("previous block");
return block; return block;
} else { } else {
// dbg!("starting lex");
let (output, err) = lex(contents, new_span.start, &[], &[], false); let (output, err) = lex(contents, new_span.start, &[], &[], false);
// dbg!("finished lex");
// dbg!(&output);
if let Some(err) = err { if let Some(err) = err {
working_set.error(err) working_set.error(err)
} }

View File

@ -300,7 +300,7 @@ fn parse_long_duration() {
"78.797877879789789sec" | into duration "78.797877879789789sec" | into duration
"#); "#);
assert_eq!(actual.out, "1min 18sec 797ms"); assert_eq!(actual.out, "1min 18sec 797ms 877µs 879ns");
} }
#[rstest] #[rstest]