diff --git a/crates/nu-command/tests/commands/math/mod.rs b/crates/nu-command/tests/commands/math/mod.rs index 76f71c700a..3f18f5798a 100644 --- a/crates/nu-command/tests/commands/math/mod.rs +++ b/crates/nu-command/tests/commands/math/mod.rs @@ -402,6 +402,18 @@ fn duration_decimal_math_with_all_units() { assert_eq!(actual.out, "1wk 3day 8hr 10min 16sec 121ms 11µs 12ns"); } +#[test] +fn duration_decimal_dans_test() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 3.14sec + "# + )); + + assert_eq!(actual.out, "3sec 140ms"); +} + #[test] fn duration_math_with_negative() { let actual = nu!( diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 530dc2646b..3d1e02fd40 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -1538,20 +1538,36 @@ fn compute(size: i64, unit: Unit, span: Span) -> Value { val: size * 1000 * 1000 * 1000, span, }, - Unit::Minute => Value::Duration { - val: size * 1000 * 1000 * 1000 * 60, - span, + Unit::Minute => match size.checked_mul(1000 * 1000 * 1000 * 60) { + Some(val) => Value::Duration { val, span }, + None => Value::Error { + error: ShellError::GenericError( + "duration too large".into(), + "duration too large".into(), + Some(span), + None, + Vec::new(), + ), + }, }, - Unit::Hour => Value::Duration { - val: size * 1000 * 1000 * 1000 * 60 * 60, - span, + Unit::Hour => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60) { + Some(val) => Value::Duration { val, span }, + None => Value::Error { + error: ShellError::GenericError( + "duration too large".into(), + "duration too large".into(), + Some(span), + None, + Vec::new(), + ), + }, }, Unit::Day => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24) { Some(val) => Value::Duration { val, span }, None => Value::Error { error: ShellError::GenericError( - "day duration too large".into(), - "day duration too large".into(), + "duration too large".into(), + "duration too large".into(), Some(span), None, Vec::new(), @@ -1562,8 +1578,8 @@ fn compute(size: i64, unit: Unit, span: Span) -> Value { Some(val) => Value::Duration { val, span }, None => Value::Error { error: ShellError::GenericError( - "week duration too large".into(), - "week duration too large".into(), + "duration too large".into(), + "duration too large".into(), Some(span), None, Vec::new(), diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index c566b56305..42d0b1e6c4 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -2175,23 +2175,50 @@ pub fn parse_duration( } } -pub fn parse_duration_bytes(bytes: &[u8], span: Span) -> Option { - fn parse_decimal_str_to_number(decimal: &str) -> Option { - let string_to_parse = format!("0.{}", decimal); - if let Ok(x) = string_to_parse.parse::() { - return Some((1_f64 / x) as i64); +// Borrowed from libm at https://github.com/rust-lang/libm/blob/master/src/math/modf.rs +pub fn modf(x: f64) -> (f64, f64) { + let rv2: f64; + let mut u = x.to_bits(); + let e = ((u >> 52 & 0x7ff) as i32) - 0x3ff; + + /* no fractional part */ + if e >= 52 { + rv2 = x; + if e == 0x400 && (u << 12) != 0 { + /* nan */ + return (x, rv2); } - None + u &= 1 << 63; + return (f64::from_bits(u), rv2); } - if bytes.is_empty() || (!bytes[0].is_ascii_digit() && bytes[0] != b'-') { + /* no integral part*/ + if e < 0 { + u &= 1 << 63; + rv2 = f64::from_bits(u); + return (x, rv2); + } + + let mask = ((!0) >> 12) >> e; + if (u & mask) == 0 { + rv2 = x; + u &= 1 << 63; + return (f64::from_bits(u), rv2); + } + u &= !mask; + rv2 = f64::from_bits(u); + (x - rv2, rv2) +} + +pub fn parse_duration_bytes(num_with_unit_bytes: &[u8], span: Span) -> Option { + if num_with_unit_bytes.is_empty() + || (!num_with_unit_bytes[0].is_ascii_digit() && num_with_unit_bytes[0] != b'-') + { return None; } - let token = String::from_utf8_lossy(bytes).to_string(); - - let upper = token.to_uppercase(); - + let num_with_unit = String::from_utf8_lossy(num_with_unit_bytes).to_string(); + let uppercase_num_with_unit = num_with_unit.to_uppercase(); let unit_groups = [ (Unit::Nanosecond, "NS", None), (Unit::Microsecond, "US", Some((Unit::Nanosecond, 1000))), @@ -2202,34 +2229,33 @@ pub fn parse_duration_bytes(bytes: &[u8], span: Span) -> Option { (Unit::Day, "DAY", Some((Unit::Minute, 1440))), (Unit::Week, "WK", Some((Unit::Day, 7))), ]; - if let Some(unit) = unit_groups.iter().find(|&x| upper.ends_with(x.1)) { - let mut lhs = token; + + if let Some(unit) = unit_groups + .iter() + .find(|&x| uppercase_num_with_unit.ends_with(x.1)) + { + let mut lhs = num_with_unit; for _ in 0..unit.1.len() { lhs.pop(); } - let input: Vec<&str> = lhs.split('.').collect(); - let (value, unit_to_use) = match &input[..] { - [number_str] => (number_str.parse::().ok(), unit.0), - [number_str, decimal_part_str] => match unit.2 { - Some(unit_to_convert_to) => match ( - number_str.parse::(), - parse_decimal_str_to_number(decimal_part_str), - ) { - (Ok(number), Some(decimal_part)) => ( - Some( - (number * unit_to_convert_to.1) + (unit_to_convert_to.1 / decimal_part), - ), - unit_to_convert_to.0, - ), - _ => (None, unit.0), - }, - None => (None, unit.0), - }, - _ => (None, unit.0), + let (decimal_part, number_part) = modf(match lhs.parse::() { + Ok(x) => x, + Err(_) => return None, + }); + + let (num, unit_to_use) = match unit.2 { + Some(unit_to_convert_to) => ( + Some( + ((number_part * unit_to_convert_to.1 as f64) + + (decimal_part * unit_to_convert_to.1 as f64)) as i64, + ), + unit_to_convert_to.0, + ), + None => (Some(number_part as i64), unit.0), }; - if let Some(x) = value { + if let Some(x) = num { trace!("-- found {} {:?}", x, unit_to_use); let lhs_span = Span::new(span.start, span.start + lhs.len()); @@ -2262,33 +2288,32 @@ pub fn parse_filesize( working_set: &StateWorkingSet, span: Span, ) -> (Expression, Option) { - trace!("parsing: duration"); - - fn parse_decimal_str_to_number(decimal: &str) -> Option { - let string_to_parse = format!("0.{}", decimal); - if let Ok(x) = string_to_parse.parse::() { - return Some((1_f64 / x) as i64); - } - None - } + trace!("parsing: filesize"); let bytes = working_set.get_span_contents(span); - if bytes.is_empty() || (!bytes[0].is_ascii_digit() && bytes[0] != b'-') { - return ( + match parse_filesize_bytes(bytes, span) { + Some(expression) => (expression, None), + None => ( garbage(span), Some(ParseError::Mismatch( "filesize".into(), "non-filesize unit".into(), span, )), - ); + ), + } +} + +pub fn parse_filesize_bytes(num_with_unit_bytes: &[u8], span: Span) -> Option { + if num_with_unit_bytes.is_empty() + || (!num_with_unit_bytes[0].is_ascii_digit() && num_with_unit_bytes[0] != b'-') + { + return None; } - let token = String::from_utf8_lossy(bytes).to_string(); - - let upper = token.to_uppercase(); - + let num_with_unit = String::from_utf8_lossy(num_with_unit_bytes).to_string(); + let uppercase_num_with_unit = num_with_unit.to_uppercase(); let unit_groups = [ (Unit::Kilobyte, "KB", Some((Unit::Byte, 1000))), (Unit::Megabyte, "MB", Some((Unit::Kilobyte, 1000))), @@ -2306,69 +2331,58 @@ pub fn parse_filesize( (Unit::Zebibyte, "ZIB", Some((Unit::Exbibyte, 1024))), (Unit::Byte, "B", None), ]; - if let Some(unit) = unit_groups.iter().find(|&x| upper.ends_with(x.1)) { - let mut lhs = token; + + if let Some(unit) = unit_groups + .iter() + .find(|&x| uppercase_num_with_unit.ends_with(x.1)) + { + let mut lhs = num_with_unit; for _ in 0..unit.1.len() { lhs.pop(); } - let input: Vec<&str> = lhs.split('.').collect(); - let (value, unit_to_use) = match &input[..] { - [number_str] => (number_str.parse::().ok(), unit.0), - [number_str, decimal_part_str] => match unit.2 { - Some(unit_to_convert_to) => match ( - number_str.parse::(), - parse_decimal_str_to_number(decimal_part_str), - ) { - (Ok(number), Some(decimal_part)) => ( - Some( - (number * unit_to_convert_to.1) + (unit_to_convert_to.1 / decimal_part), - ), - unit_to_convert_to.0, - ), - _ => (None, unit.0), - }, - None => (None, unit.0), - }, - _ => (None, unit.0), + let (decimal_part, number_part) = modf(match lhs.parse::() { + Ok(x) => x, + Err(_) => return None, + }); + + let (num, unit_to_use) = match unit.2 { + Some(unit_to_convert_to) => ( + Some( + ((number_part * unit_to_convert_to.1 as f64) + + (decimal_part * unit_to_convert_to.1 as f64)) as i64, + ), + unit_to_convert_to.0, + ), + None => (Some(number_part as i64), unit.0), }; - if let Some(x) = value { + if let Some(x) = num { trace!("-- found {} {:?}", x, unit_to_use); let lhs_span = Span::new(span.start, span.start + lhs.len()); let unit_span = Span::new(span.start + lhs.len(), span.end); - return ( - Expression { - expr: Expr::ValueWithUnit( - Box::new(Expression { - expr: Expr::Int(x), - span: lhs_span, - ty: Type::Number, - custom_completion: None, - }), - Spanned { - item: unit_to_use, - span: unit_span, - }, - ), - span, - ty: Type::Filesize, - custom_completion: None, - }, - None, - ); + return Some(Expression { + expr: Expr::ValueWithUnit( + Box::new(Expression { + expr: Expr::Int(x), + span: lhs_span, + ty: Type::Number, + custom_completion: None, + }), + Spanned { + item: unit_to_use, + span: unit_span, + }, + ), + span, + ty: Type::Filesize, + custom_completion: None, + }); } } - ( - garbage(span), - Some(ParseError::Mismatch( - "filesize".into(), - "non-filesize unit".into(), - span, - )), - ) + None } pub fn parse_glob_pattern(