diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 816b2fda534..e091f232079 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -74,6 +74,7 @@ fn parse_suffix(s: &str) -> Result<(f64, Option)> { } let suffix = match iter.next_back() { Some('K') => Some((RawSuffix::K, with_i)), + Some('k') => Some((RawSuffix::K, with_i)), Some('M') => Some((RawSuffix::M, with_i)), Some('G') => Some((RawSuffix::G, with_i)), Some('T') => Some((RawSuffix::T, with_i)), @@ -81,8 +82,20 @@ fn parse_suffix(s: &str) -> Result<(f64, Option)> { Some('E') => Some((RawSuffix::E, with_i)), Some('Z') => Some((RawSuffix::Z, with_i)), Some('Y') => Some((RawSuffix::Y, with_i)), + Some('R') => Some((RawSuffix::R, with_i)), + Some('Q') => Some((RawSuffix::Q, with_i)), Some('0'..='9') if !with_i => None, _ => { + // If with_i is true, the string ends with 'i' but there's no valid suffix letter + // This is always an invalid suffix (e.g., "1i", "2Ai") + if with_i { + return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote())); + } + // For other cases, check if the number part (without the last character) is valid + let number_part = &s[..s.len() - 1]; + if number_part.is_empty() || number_part.parse::().is_err() { + return Err(translate!("numfmt-error-invalid-number", "input" => s.quote())); + } return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote())); } }; @@ -123,6 +136,8 @@ fn remove_suffix(i: f64, s: Option, u: &Unit) -> Result { RawSuffix::E => Ok(i * 1e18), RawSuffix::Z => Ok(i * 1e21), RawSuffix::Y => Ok(i * 1e24), + RawSuffix::R => Ok(i * 1e27), + RawSuffix::Q => Ok(i * 1e30), }, (Some((raw_suffix, false)), &Unit::Iec(false)) | (Some((raw_suffix, true)), &Unit::Auto | &Unit::Iec(true)) => match raw_suffix { @@ -134,6 +149,8 @@ fn remove_suffix(i: f64, s: Option, u: &Unit) -> Result { RawSuffix::E => Ok(i * IEC_BASES[6]), RawSuffix::Z => Ok(i * IEC_BASES[7]), RawSuffix::Y => Ok(i * IEC_BASES[8]), + RawSuffix::R => Ok(i * IEC_BASES[9]), + RawSuffix::Q => Ok(i * IEC_BASES[10]), }, (Some((raw_suffix, false)), &Unit::Iec(true)) => Err( translate!("numfmt-error-missing-i-suffix", "number" => i, "suffix" => format!("{raw_suffix:?}")), @@ -212,10 +229,10 @@ fn consider_suffix( round_method: RoundMethod, precision: usize, ) -> Result<(f64, Option)> { - use crate::units::RawSuffix::{E, G, K, M, P, T, Y, Z}; + use crate::units::RawSuffix::{E, G, K, M, P, Q, R, T, Y, Z}; let abs_n = n.abs(); - let suffixes = [K, M, G, T, P, E, Z, Y]; + let suffixes = [K, M, G, T, P, E, Z, Y, R, Q]; let (bases, with_i) = match *u { Unit::Si => (&SI_BASES, false), @@ -234,6 +251,8 @@ fn consider_suffix( _ if abs_n < bases[7] => 6, _ if abs_n < bases[8] => 7, _ if abs_n < bases[9] => 8, + _ if abs_n < bases[10] => 9, + _ if abs_n < bases[10] * 1000.0 => 10, _ => return Err(translate!("numfmt-error-number-too-big")), }; @@ -334,38 +353,41 @@ fn format_string( fn format_and_print_delimited(s: &str, options: &NumfmtOptions) -> Result<()> { let delimiter = options.delimiter.as_ref().unwrap(); + let mut output = String::new(); for (n, field) in (1..).zip(s.split(delimiter)) { let field_selected = uucore::ranges::contain(&options.fields, n); - // print delimiter before second and subsequent fields + // add delimiter before second and subsequent fields if n > 1 { - print!("{delimiter}"); + output.push_str(delimiter); } if field_selected { - print!("{}", format_string(field.trim_start(), options, None)?); + output.push_str(&format_string(field.trim_start(), options, None)?); } else { - // print unselected field without conversion - print!("{field}"); + // add unselected field without conversion + output.push_str(field); } } - println!(); + println!("{output}"); Ok(()) } fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> { + let mut output = String::new(); + for (n, (prefix, field)) in (1..).zip(WhitespaceSplitter { s: Some(s) }) { let field_selected = uucore::ranges::contain(&options.fields, n); if field_selected { let empty_prefix = prefix.is_empty(); - // print delimiter before second and subsequent fields + // add delimiter before second and subsequent fields let prefix = if n > 1 { - print!(" "); + output.push(' '); &prefix[1..] } else { prefix @@ -377,22 +399,24 @@ fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> { None }; - print!("{}", format_string(field, options, implicit_padding)?); + output.push_str(&format_string(field, options, implicit_padding)?); } else { // the -z option converts an initial \n into a space let prefix = if options.zero_terminated && prefix.starts_with('\n') { - print!(" "); + output.push(' '); &prefix[1..] } else { prefix }; - // print unselected field without conversion - print!("{prefix}{field}"); + // add unselected field without conversion + output.push_str(prefix); + output.push_str(field); } } let eol = if options.zero_terminated { '\0' } else { '\n' }; - print!("{eol}"); + output.push(eol); + print!("{output}"); Ok(()) } @@ -445,4 +469,123 @@ mod tests { assert_eq!(2, parse_implicit_precision("1.23K")); assert_eq!(3, parse_implicit_precision("1.234K")); } + + #[test] + fn test_parse_suffix_q_r_k() { + let result = parse_suffix("1Q"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 1.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); + assert!(!with_i); + + let result = parse_suffix("2R"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 2.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::R as i32); + assert!(!with_i); + + let result = parse_suffix("3k"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 3.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::K as i32); + assert!(!with_i); + + let result = parse_suffix("4Qi"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 4.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); + assert!(with_i); + + let result = parse_suffix("5Ri"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 5.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::R as i32); + assert!(with_i); + } + + #[test] + fn test_parse_suffix_error_messages() { + let result = parse_suffix("foo"); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(error.contains("numfmt-error-invalid-number") || error.contains("invalid number")); + assert!(!error.contains("invalid suffix")); + + let result = parse_suffix("World"); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(error.contains("numfmt-error-invalid-number") || error.contains("invalid number")); + assert!(!error.contains("invalid suffix")); + + let result = parse_suffix("123i"); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(error.contains("numfmt-error-invalid-suffix") || error.contains("invalid suffix")); + } + + #[test] + fn test_remove_suffix_q_r() { + use crate::units::Unit; + + let result = remove_suffix(1.0, Some((RawSuffix::Q, false)), &Unit::Si); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1e30); + + let result = remove_suffix(1.0, Some((RawSuffix::R, false)), &Unit::Si); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1e27); + + let result = remove_suffix(1.0, Some((RawSuffix::Q, true)), &Unit::Iec(true)); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), IEC_BASES[10]); + + let result = remove_suffix(1.0, Some((RawSuffix::R, true)), &Unit::Iec(true)); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), IEC_BASES[9]); + } + + #[test] + fn test_consider_suffix_q_r() { + use crate::options::RoundMethod; + use crate::units::Unit; + + let result = consider_suffix(1e27, &Unit::Si, RoundMethod::FromZero, 0); + assert!(result.is_ok()); + let (value, suffix) = result.unwrap(); + assert!(suffix.is_some()); + let (raw_suffix, _) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::R as i32); + assert_eq!(value, 1.0); + + let result = consider_suffix(1e30, &Unit::Si, RoundMethod::FromZero, 0); + assert!(result.is_ok()); + let (value, suffix) = result.unwrap(); + assert!(suffix.is_some()); + let (raw_suffix, _) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); + assert_eq!(value, 1.0); + + let result = consider_suffix(5e30, &Unit::Si, RoundMethod::FromZero, 0); + assert!(result.is_ok()); + let (value, suffix) = result.unwrap(); + assert!(suffix.is_some()); + let (raw_suffix, _) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); + assert_eq!(value, 5.0); + } } diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index f472b359363..abeaca25698 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -460,9 +460,9 @@ mod tests { let result_display = format!("{result}"); assert_eq!( result_debug, - "FormattingError(\"numfmt-error-invalid-suffix\")" + "FormattingError(\"numfmt-error-invalid-number\")" ); - assert_eq!(result_display, "numfmt-error-invalid-suffix"); + assert_eq!(result_display, "numfmt-error-invalid-number"); assert_eq!(result.code(), 2); } diff --git a/src/uu/numfmt/src/units.rs b/src/uu/numfmt/src/units.rs index c52dee20c02..bc5d480bef6 100644 --- a/src/uu/numfmt/src/units.rs +++ b/src/uu/numfmt/src/units.rs @@ -4,9 +4,9 @@ // file that was distributed with this source code. use std::fmt; -pub const SI_BASES: [f64; 10] = [1., 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21, 1e24, 1e27]; +pub const SI_BASES: [f64; 11] = [1., 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21, 1e24, 1e27, 1e30]; -pub const IEC_BASES: [f64; 10] = [ +pub const IEC_BASES: [f64; 11] = [ 1., 1_024., 1_048_576., @@ -17,6 +17,7 @@ pub const IEC_BASES: [f64; 10] = [ 1_180_591_620_717_411_303_424., 1_208_925_819_614_629_174_706_176., 1_237_940_039_285_380_274_899_124_224., + 1_267_650_600_228_229_401_496_703_205_376., ]; pub type WithI = bool; @@ -41,6 +42,8 @@ pub enum RawSuffix { E, Z, Y, + R, + Q, } pub type Suffix = (RawSuffix, WithI); @@ -60,6 +63,8 @@ impl fmt::Display for DisplayableSuffix { (RawSuffix::E, _) => write!(f, "E"), (RawSuffix::Z, _) => write!(f, "Z"), (RawSuffix::Y, _) => write!(f, "Y"), + (RawSuffix::R, _) => write!(f, "R"), + (RawSuffix::Q, _) => write!(f, "Q"), } .and_then(|()| match with_i { true => write!(f, "i"), diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 673073694a7..d947833f7c7 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -241,8 +241,7 @@ fn test_should_report_invalid_empty_number_on_blank_stdin() { #[test] fn test_suffixes() { - // TODO add support for ronna (R) and quetta (Q) - let valid_suffixes = ['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' /*'R' , 'Q'*/]; + let valid_suffixes = ['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q', 'k']; for c in ('A'..='Z').chain('a'..='z') { let args = ["--from=si", "--to=si", &format!("1{c}")]; @@ -264,12 +263,12 @@ fn test_suffixes() { #[test] fn test_should_report_invalid_suffix_on_nan() { - // GNU numfmt reports this one as “invalid number” + // GNU numfmt reports this one as "invalid number" new_ucmd!() .args(&["--from=auto"]) .pipe_in("NaN") .fails() - .stderr_is("numfmt: invalid suffix in input: 'NaN'\n"); + .stderr_is("numfmt: invalid number: 'NaN'\n"); } #[test] @@ -700,7 +699,7 @@ fn test_invalid_stdin_number_with_warn_returns_status_0() { .pipe_in("4Q") .succeeds() .stdout_is("4Q\n") - .stderr_is("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -718,7 +717,7 @@ fn test_invalid_stdin_number_with_abort_returns_status_2() { .args(&["--invalid=abort"]) .pipe_in("4Q") .fails_with_code(2) - .stderr_only("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_only("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -728,7 +727,7 @@ fn test_invalid_stdin_number_with_fail_returns_status_2() { .pipe_in("4Q") .fails_with_code(2) .stdout_is("4Q\n") - .stderr_is("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -737,7 +736,7 @@ fn test_invalid_arg_number_with_warn_returns_status_0() { .args(&["--invalid=warn", "4Q"]) .succeeds() .stdout_is("4Q\n") - .stderr_is("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -753,7 +752,7 @@ fn test_invalid_arg_number_with_abort_returns_status_2() { new_ucmd!() .args(&["--invalid=abort", "4Q"]) .fails_with_code(2) - .stderr_only("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_only("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -762,7 +761,7 @@ fn test_invalid_arg_number_with_fail_returns_status_2() { .args(&["--invalid=fail", "4Q"]) .fails_with_code(2) .stdout_is("4Q\n") - .stderr_is("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test]