Skip to content

Commit 907c573

Browse files
committed
numfmt: add Q/R/k suffixes and fix bugs
- Add quetta (Q), ronna (R) suffix support (10^30, 10^27) - Support lowercase 'k' suffix - Fix "invalid number" vs "invalid suffix" error distinction - Fix output duplication on formatting errors - Add regression tests Improves GNU test suite compatibility.
1 parent 73d1bce commit 907c573

File tree

4 files changed

+176
-29
lines changed

4 files changed

+176
-29
lines changed

src/uu/numfmt/src/format.rs

Lines changed: 158 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,28 @@ fn parse_suffix(s: &str) -> Result<(f64, Option<Suffix>)> {
7474
}
7575
let suffix = match iter.next_back() {
7676
Some('K') => Some((RawSuffix::K, with_i)),
77+
Some('k') => Some((RawSuffix::K, with_i)),
7778
Some('M') => Some((RawSuffix::M, with_i)),
7879
Some('G') => Some((RawSuffix::G, with_i)),
7980
Some('T') => Some((RawSuffix::T, with_i)),
8081
Some('P') => Some((RawSuffix::P, with_i)),
8182
Some('E') => Some((RawSuffix::E, with_i)),
8283
Some('Z') => Some((RawSuffix::Z, with_i)),
8384
Some('Y') => Some((RawSuffix::Y, with_i)),
85+
Some('R') => Some((RawSuffix::R, with_i)),
86+
Some('Q') => Some((RawSuffix::Q, with_i)),
8487
Some('0'..='9') if !with_i => None,
8588
_ => {
89+
// If with_i is true, the string ends with 'i' but there's no valid suffix letter
90+
// This is always an invalid suffix (e.g., "1i", "2Ai")
91+
if with_i {
92+
return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote()));
93+
}
94+
// For other cases, check if the number part (without the last character) is valid
95+
let number_part = &s[..s.len() - 1];
96+
if number_part.is_empty() || number_part.parse::<f64>().is_err() {
97+
return Err(translate!("numfmt-error-invalid-number", "input" => s.quote()));
98+
}
8699
return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote()));
87100
}
88101
};
@@ -123,6 +136,8 @@ fn remove_suffix(i: f64, s: Option<Suffix>, u: &Unit) -> Result<f64> {
123136
RawSuffix::E => Ok(i * 1e18),
124137
RawSuffix::Z => Ok(i * 1e21),
125138
RawSuffix::Y => Ok(i * 1e24),
139+
RawSuffix::R => Ok(i * 1e27),
140+
RawSuffix::Q => Ok(i * 1e30),
126141
},
127142
(Some((raw_suffix, false)), &Unit::Iec(false))
128143
| (Some((raw_suffix, true)), &Unit::Auto | &Unit::Iec(true)) => match raw_suffix {
@@ -134,6 +149,8 @@ fn remove_suffix(i: f64, s: Option<Suffix>, u: &Unit) -> Result<f64> {
134149
RawSuffix::E => Ok(i * IEC_BASES[6]),
135150
RawSuffix::Z => Ok(i * IEC_BASES[7]),
136151
RawSuffix::Y => Ok(i * IEC_BASES[8]),
152+
RawSuffix::R => Ok(i * IEC_BASES[9]),
153+
RawSuffix::Q => Ok(i * IEC_BASES[10]),
137154
},
138155
(Some((raw_suffix, false)), &Unit::Iec(true)) => Err(
139156
translate!("numfmt-error-missing-i-suffix", "number" => i, "suffix" => format!("{raw_suffix:?}")),
@@ -212,10 +229,10 @@ fn consider_suffix(
212229
round_method: RoundMethod,
213230
precision: usize,
214231
) -> Result<(f64, Option<Suffix>)> {
215-
use crate::units::RawSuffix::{E, G, K, M, P, T, Y, Z};
232+
use crate::units::RawSuffix::{E, G, K, M, P, Q, R, T, Y, Z};
216233

217234
let abs_n = n.abs();
218-
let suffixes = [K, M, G, T, P, E, Z, Y];
235+
let suffixes = [K, M, G, T, P, E, Z, Y, R, Q];
219236

220237
let (bases, with_i) = match *u {
221238
Unit::Si => (&SI_BASES, false),
@@ -234,6 +251,8 @@ fn consider_suffix(
234251
_ if abs_n < bases[7] => 6,
235252
_ if abs_n < bases[8] => 7,
236253
_ if abs_n < bases[9] => 8,
254+
_ if abs_n < bases[10] => 9,
255+
_ if abs_n < bases[10] * 1000.0 => 10,
237256
_ => return Err(translate!("numfmt-error-number-too-big")),
238257
};
239258

@@ -334,38 +353,41 @@ fn format_string(
334353

335354
fn format_and_print_delimited(s: &str, options: &NumfmtOptions) -> Result<()> {
336355
let delimiter = options.delimiter.as_ref().unwrap();
356+
let mut output = String::new();
337357

338358
for (n, field) in (1..).zip(s.split(delimiter)) {
339359
let field_selected = uucore::ranges::contain(&options.fields, n);
340360

341-
// print delimiter before second and subsequent fields
361+
// add delimiter before second and subsequent fields
342362
if n > 1 {
343-
print!("{delimiter}");
363+
output.push_str(delimiter);
344364
}
345365

346366
if field_selected {
347-
print!("{}", format_string(field.trim_start(), options, None)?);
367+
output.push_str(&format_string(field.trim_start(), options, None)?);
348368
} else {
349-
// print unselected field without conversion
350-
print!("{field}");
369+
// add unselected field without conversion
370+
output.push_str(field);
351371
}
352372
}
353373

354-
println!();
374+
println!("{output}");
355375

356376
Ok(())
357377
}
358378

359379
fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> {
380+
let mut output = String::new();
381+
360382
for (n, (prefix, field)) in (1..).zip(WhitespaceSplitter { s: Some(s) }) {
361383
let field_selected = uucore::ranges::contain(&options.fields, n);
362384

363385
if field_selected {
364386
let empty_prefix = prefix.is_empty();
365387

366-
// print delimiter before second and subsequent fields
388+
// add delimiter before second and subsequent fields
367389
let prefix = if n > 1 {
368-
print!(" ");
390+
output.push(' ');
369391
&prefix[1..]
370392
} else {
371393
prefix
@@ -377,22 +399,24 @@ fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> {
377399
None
378400
};
379401

380-
print!("{}", format_string(field, options, implicit_padding)?);
402+
output.push_str(&format_string(field, options, implicit_padding)?);
381403
} else {
382404
// the -z option converts an initial \n into a space
383405
let prefix = if options.zero_terminated && prefix.starts_with('\n') {
384-
print!(" ");
406+
output.push(' ');
385407
&prefix[1..]
386408
} else {
387409
prefix
388410
};
389-
// print unselected field without conversion
390-
print!("{prefix}{field}");
411+
// add unselected field without conversion
412+
output.push_str(prefix);
413+
output.push_str(field);
391414
}
392415
}
393416

394417
let eol = if options.zero_terminated { '\0' } else { '\n' };
395-
print!("{eol}");
418+
output.push(eol);
419+
print!("{output}");
396420

397421
Ok(())
398422
}
@@ -445,4 +469,123 @@ mod tests {
445469
assert_eq!(2, parse_implicit_precision("1.23K"));
446470
assert_eq!(3, parse_implicit_precision("1.234K"));
447471
}
472+
473+
#[test]
474+
fn test_parse_suffix_q_r_k() {
475+
let result = parse_suffix("1Q");
476+
assert!(result.is_ok());
477+
let (number, suffix) = result.unwrap();
478+
assert_eq!(number, 1.0);
479+
assert!(suffix.is_some());
480+
let (raw_suffix, with_i) = suffix.unwrap();
481+
assert_eq!(raw_suffix as i32, RawSuffix::Q as i32);
482+
assert!(!with_i);
483+
484+
let result = parse_suffix("2R");
485+
assert!(result.is_ok());
486+
let (number, suffix) = result.unwrap();
487+
assert_eq!(number, 2.0);
488+
assert!(suffix.is_some());
489+
let (raw_suffix, with_i) = suffix.unwrap();
490+
assert_eq!(raw_suffix as i32, RawSuffix::R as i32);
491+
assert!(!with_i);
492+
493+
let result = parse_suffix("3k");
494+
assert!(result.is_ok());
495+
let (number, suffix) = result.unwrap();
496+
assert_eq!(number, 3.0);
497+
assert!(suffix.is_some());
498+
let (raw_suffix, with_i) = suffix.unwrap();
499+
assert_eq!(raw_suffix as i32, RawSuffix::K as i32);
500+
assert!(!with_i);
501+
502+
let result = parse_suffix("4Qi");
503+
assert!(result.is_ok());
504+
let (number, suffix) = result.unwrap();
505+
assert_eq!(number, 4.0);
506+
assert!(suffix.is_some());
507+
let (raw_suffix, with_i) = suffix.unwrap();
508+
assert_eq!(raw_suffix as i32, RawSuffix::Q as i32);
509+
assert!(with_i);
510+
511+
let result = parse_suffix("5Ri");
512+
assert!(result.is_ok());
513+
let (number, suffix) = result.unwrap();
514+
assert_eq!(number, 5.0);
515+
assert!(suffix.is_some());
516+
let (raw_suffix, with_i) = suffix.unwrap();
517+
assert_eq!(raw_suffix as i32, RawSuffix::R as i32);
518+
assert!(with_i);
519+
}
520+
521+
#[test]
522+
fn test_parse_suffix_error_messages() {
523+
let result = parse_suffix("foo");
524+
assert!(result.is_err());
525+
let error = result.unwrap_err();
526+
assert!(error.contains("numfmt-error-invalid-number") || error.contains("invalid number"));
527+
assert!(!error.contains("invalid suffix"));
528+
529+
let result = parse_suffix("World");
530+
assert!(result.is_err());
531+
let error = result.unwrap_err();
532+
assert!(error.contains("numfmt-error-invalid-number") || error.contains("invalid number"));
533+
assert!(!error.contains("invalid suffix"));
534+
535+
let result = parse_suffix("123i");
536+
assert!(result.is_err());
537+
let error = result.unwrap_err();
538+
assert!(error.contains("numfmt-error-invalid-suffix") || error.contains("invalid suffix"));
539+
}
540+
541+
#[test]
542+
fn test_remove_suffix_q_r() {
543+
use crate::units::Unit;
544+
545+
let result = remove_suffix(1.0, Some((RawSuffix::Q, false)), &Unit::Si);
546+
assert!(result.is_ok());
547+
assert_eq!(result.unwrap(), 1e30);
548+
549+
let result = remove_suffix(1.0, Some((RawSuffix::R, false)), &Unit::Si);
550+
assert!(result.is_ok());
551+
assert_eq!(result.unwrap(), 1e27);
552+
553+
let result = remove_suffix(1.0, Some((RawSuffix::Q, true)), &Unit::Iec(true));
554+
assert!(result.is_ok());
555+
assert_eq!(result.unwrap(), IEC_BASES[10]);
556+
557+
let result = remove_suffix(1.0, Some((RawSuffix::R, true)), &Unit::Iec(true));
558+
assert!(result.is_ok());
559+
assert_eq!(result.unwrap(), IEC_BASES[9]);
560+
}
561+
562+
#[test]
563+
fn test_consider_suffix_q_r() {
564+
use crate::options::RoundMethod;
565+
use crate::units::Unit;
566+
567+
let result = consider_suffix(1e27, &Unit::Si, RoundMethod::FromZero, 0);
568+
assert!(result.is_ok());
569+
let (value, suffix) = result.unwrap();
570+
assert!(suffix.is_some());
571+
let (raw_suffix, _) = suffix.unwrap();
572+
assert_eq!(raw_suffix as i32, RawSuffix::R as i32);
573+
assert_eq!(value, 1.0);
574+
575+
let result = consider_suffix(1e30, &Unit::Si, RoundMethod::FromZero, 0);
576+
assert!(result.is_ok());
577+
let (value, suffix) = result.unwrap();
578+
assert!(suffix.is_some());
579+
let (raw_suffix, _) = suffix.unwrap();
580+
assert_eq!(raw_suffix as i32, RawSuffix::Q as i32);
581+
assert_eq!(value, 1.0);
582+
583+
let result = consider_suffix(5e30, &Unit::Si, RoundMethod::FromZero, 0);
584+
assert!(result.is_ok());
585+
let (value, suffix) = result.unwrap();
586+
assert!(suffix.is_some());
587+
let (raw_suffix, _) = suffix.unwrap();
588+
assert_eq!(raw_suffix as i32, RawSuffix::Q as i32);
589+
assert_eq!(value, 5.0);
590+
}
448591
}

src/uu/numfmt/src/numfmt.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,9 +460,9 @@ mod tests {
460460
let result_display = format!("{result}");
461461
assert_eq!(
462462
result_debug,
463-
"FormattingError(\"numfmt-error-invalid-suffix\")"
463+
"FormattingError(\"numfmt-error-invalid-number\")"
464464
);
465-
assert_eq!(result_display, "numfmt-error-invalid-suffix");
465+
assert_eq!(result_display, "numfmt-error-invalid-number");
466466
assert_eq!(result.code(), 2);
467467
}
468468

src/uu/numfmt/src/units.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
// file that was distributed with this source code.
55
use std::fmt;
66

7-
pub const SI_BASES: [f64; 10] = [1., 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21, 1e24, 1e27];
7+
pub const SI_BASES: [f64; 11] = [1., 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21, 1e24, 1e27, 1e30];
88

9-
pub const IEC_BASES: [f64; 10] = [
9+
pub const IEC_BASES: [f64; 11] = [
1010
1.,
1111
1_024.,
1212
1_048_576.,
@@ -17,6 +17,7 @@ pub const IEC_BASES: [f64; 10] = [
1717
1_180_591_620_717_411_303_424.,
1818
1_208_925_819_614_629_174_706_176.,
1919
1_237_940_039_285_380_274_899_124_224.,
20+
1_267_650_600_228_229_401_496_703_205_376.,
2021
];
2122

2223
pub type WithI = bool;
@@ -41,6 +42,8 @@ pub enum RawSuffix {
4142
E,
4243
Z,
4344
Y,
45+
R,
46+
Q,
4447
}
4548

4649
pub type Suffix = (RawSuffix, WithI);
@@ -60,6 +63,8 @@ impl fmt::Display for DisplayableSuffix {
6063
(RawSuffix::E, _) => write!(f, "E"),
6164
(RawSuffix::Z, _) => write!(f, "Z"),
6265
(RawSuffix::Y, _) => write!(f, "Y"),
66+
(RawSuffix::R, _) => write!(f, "R"),
67+
(RawSuffix::Q, _) => write!(f, "Q"),
6368
}
6469
.and_then(|()| match with_i {
6570
true => write!(f, "i"),

0 commit comments

Comments
 (0)