Skip to content

Commit ceacc67

Browse files
authored
fix(numfmt):fix precision loss for large numbers in #11654 (#11716)
* fix(numfmt):fix recision loss for large numbers in #11654 * numfmt: fix some tests in format.rs caused by importing numeric.rs * add a file header and remove a redundant comment * remove a redundant comment * add a empty line between two functions * refactor(numfmt): rename helper functions for accuracy; fix double negation logic
1 parent d6cadd4 commit ceacc67

4 files changed

Lines changed: 115 additions & 20 deletions

File tree

src/uu/numfmt/src/format.rs

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use uucore::display::Quotable;
99
use uucore::i18n::decimal::locale_grouping_separator;
1010
use uucore::translate;
1111

12+
use crate::numeric::ParsedNumber;
1213
use crate::options::{NumfmtOptions, RoundMethod, TransformOptions};
1314
use crate::units::{
1415
DisplayableSuffix, RawSuffix, Result, Suffix, Unit, iec_bases_f64, si_bases_f64,
@@ -147,12 +148,17 @@ fn detailed_error_message(s: &str, unit: Unit, unit_separator: &str) -> Option<S
147148
None
148149
}
149150

150-
fn parse_number_part(s: &str, input: &str) -> Result<f64> {
151+
fn parse_number_part(s: &str, input: &str) -> Result<ParsedNumber> {
151152
if s.ends_with('.') {
152153
return Err(translate!("numfmt-error-invalid-number", "input" => input.quote()));
153154
}
154155

156+
if let Ok(n) = s.parse::<i128>() {
157+
return Ok(ParsedNumber::ExactInt(n));
158+
}
159+
155160
s.parse::<f64>()
161+
.map(ParsedNumber::Float)
156162
.map_err(|_| translate!("numfmt-error-invalid-number", "input" => input.quote()))
157163
}
158164

@@ -161,7 +167,7 @@ fn parse_suffix(
161167
unit: Unit,
162168
unit_separator: &str,
163169
explicit_unit_separator: bool,
164-
) -> Result<(f64, Option<Suffix>)> {
170+
) -> Result<(ParsedNumber, Option<Suffix>)> {
165171
let trimmed = s.trim_end();
166172
if trimmed.is_empty() {
167173
return Err(translate!("numfmt-error-invalid-number-empty"));
@@ -364,7 +370,22 @@ fn remove_suffix(i: f64, s: Option<Suffix>, u: Unit) -> Result<f64> {
364370
}
365371
}
366372

367-
fn transform_from(s: &str, opts: &TransformOptions, options: &NumfmtOptions) -> Result<f64> {
373+
fn try_scale_exact_int_with_from_unit(
374+
value: ParsedNumber,
375+
from_unit: usize,
376+
) -> Option<ParsedNumber> {
377+
let integer = value.exact_int()?;
378+
let from_unit = i128::try_from(from_unit).ok()?;
379+
let scaled = integer.checked_mul(from_unit)?;
380+
381+
Some(ParsedNumber::ExactInt(scaled))
382+
}
383+
384+
fn transform_from(
385+
s: &str,
386+
opts: &TransformOptions,
387+
options: &NumfmtOptions,
388+
) -> Result<ParsedNumber> {
368389
let (i, suffix) = parse_suffix(
369390
s,
370391
opts.from,
@@ -375,17 +396,24 @@ fn transform_from(s: &str, opts: &TransformOptions, options: &NumfmtOptions) ->
375396
detailed_error_message(s, opts.from, &options.unit_separator).unwrap_or(original)
376397
})?;
377398
let had_no_suffix = suffix.is_none();
378-
let i = i * (opts.from_unit as f64);
399+
400+
if had_no_suffix {
401+
if let Some(scaled) = try_scale_exact_int_with_from_unit(i, opts.from_unit) {
402+
return Ok(scaled);
403+
}
404+
}
405+
406+
let i = i.to_f64() * (opts.from_unit as f64);
379407

380408
remove_suffix(i, suffix, opts.from).map(|n| {
381-
// GNU numfmt doesn't round values if no --from argument is provided by the user
382-
if opts.from == Unit::None || had_no_suffix {
409+
let n = if opts.from == Unit::None || had_no_suffix {
383410
if n == -0.0 { 0.0 } else { n }
384411
} else if n < 0.0 {
385412
-n.abs().ceil()
386413
} else {
387414
n.ceil()
388-
}
415+
};
416+
ParsedNumber::Float(n)
389417
})
390418
}
391419

@@ -480,13 +508,43 @@ fn consider_suffix(
480508
}
481509
}
482510

511+
fn try_format_exact_int_without_suffix_scaling(
512+
value: ParsedNumber,
513+
opts: &TransformOptions,
514+
precision: usize,
515+
) -> Option<String> {
516+
if opts.to != Unit::None {
517+
return None;
518+
}
519+
520+
let integer = value.exact_int()?;
521+
let to_unit = i128::try_from(opts.to_unit).ok()?;
522+
523+
if integer % to_unit != 0 {
524+
return None;
525+
}
526+
527+
let scaled = integer / to_unit;
528+
529+
Some(if precision == 0 {
530+
scaled.to_string()
531+
} else {
532+
format!("{scaled}.{}", "0".repeat(precision))
533+
})
534+
}
535+
483536
fn transform_to(
484-
s: f64,
537+
s: ParsedNumber,
485538
opts: &TransformOptions,
486539
round_method: RoundMethod,
487540
precision: usize,
488541
unit_separator: &str,
489542
) -> Result<String> {
543+
if let Some(result) = try_format_exact_int_without_suffix_scaling(s, opts, precision) {
544+
return Ok(result);
545+
}
546+
547+
let s = s.to_f64();
490548
let i2 = s / (opts.to_unit as f64);
491549
let (i2, s) = consider_suffix(i2, opts.to, round_method, precision)?;
492550
Ok(match s {
@@ -771,7 +829,7 @@ mod tests {
771829
let result = parse_suffix("1Q", Unit::Auto, "", false);
772830
assert!(result.is_ok());
773831
let (number, suffix) = result.unwrap();
774-
assert_eq!(number, 1.0);
832+
assert_eq!(number.to_f64(), 1.0);
775833
assert!(suffix.is_some());
776834
let (raw_suffix, with_i) = suffix.unwrap();
777835
assert_eq!(raw_suffix as i32, RawSuffix::Q as i32);
@@ -780,7 +838,7 @@ mod tests {
780838
let result = parse_suffix("2R", Unit::Auto, "", false);
781839
assert!(result.is_ok());
782840
let (number, suffix) = result.unwrap();
783-
assert_eq!(number, 2.0);
841+
assert_eq!(number.to_f64(), 2.0);
784842
assert!(suffix.is_some());
785843
let (raw_suffix, with_i) = suffix.unwrap();
786844
assert_eq!(raw_suffix as i32, RawSuffix::R as i32);
@@ -789,7 +847,7 @@ mod tests {
789847
let result = parse_suffix("3k", Unit::Auto, "", false);
790848
assert!(result.is_ok());
791849
let (number, suffix) = result.unwrap();
792-
assert_eq!(number, 3.0);
850+
assert_eq!(number.to_f64(), 3.0);
793851
assert!(suffix.is_some());
794852
let (raw_suffix, with_i) = suffix.unwrap();
795853
assert_eq!(raw_suffix as i32, RawSuffix::K as i32);
@@ -798,7 +856,7 @@ mod tests {
798856
let result = parse_suffix("4Qi", Unit::Auto, "", false);
799857
assert!(result.is_ok());
800858
let (number, suffix) = result.unwrap();
801-
assert_eq!(number, 4.0);
859+
assert_eq!(number.to_f64(), 4.0);
802860
assert!(suffix.is_some());
803861
let (raw_suffix, with_i) = suffix.unwrap();
804862
assert_eq!(raw_suffix as i32, RawSuffix::Q as i32);
@@ -807,7 +865,7 @@ mod tests {
807865
let result = parse_suffix("5Ri", Unit::Auto, "", false);
808866
assert!(result.is_ok());
809867
let (number, suffix) = result.unwrap();
810-
assert_eq!(number, 5.0);
868+
assert_eq!(number.to_f64(), 5.0);
811869
assert!(suffix.is_some());
812870
let (raw_suffix, with_i) = suffix.unwrap();
813871
assert_eq!(raw_suffix as i32, RawSuffix::R as i32);
@@ -1023,9 +1081,9 @@ mod tests {
10231081

10241082
#[test]
10251083
fn test_parse_number_part_valid() {
1026-
assert_eq!(parse_number_part("42", "42").unwrap(), 42.0);
1027-
assert_eq!(parse_number_part("-3.5", "-3.5").unwrap(), -3.5);
1028-
assert_eq!(parse_number_part("0", "0").unwrap(), 0.0);
1084+
assert_eq!(parse_number_part("42", "42").unwrap().to_f64(), 42.0);
1085+
assert_eq!(parse_number_part("-3.5", "-3.5").unwrap().to_f64(), -3.5);
1086+
assert_eq!(parse_number_part("0", "0").unwrap().to_f64(), 0.0);
10291087
}
10301088

10311089
#[test]
@@ -1094,15 +1152,21 @@ mod tests {
10941152
#[test]
10951153
fn test_parse_number_part_large_and_tiny() {
10961154
assert_eq!(
1097-
parse_number_part("999999999999", "999999999999").unwrap(),
1155+
parse_number_part("999999999999", "999999999999")
1156+
.unwrap()
1157+
.to_f64(),
10981158
999_999_999_999.0
10991159
);
11001160
assert_eq!(
1101-
parse_number_part("0.000000001", "0.000000001").unwrap(),
1161+
parse_number_part("0.000000001", "0.000000001")
1162+
.unwrap()
1163+
.to_f64(),
11021164
0.000_000_001
11031165
);
11041166
assert_eq!(
1105-
parse_number_part("-99999999", "-99999999").unwrap(),
1167+
parse_number_part("-99999999", "-99999999")
1168+
.unwrap()
1169+
.to_f64(),
11061170
-99_999_999.0
11071171
);
11081172
}

src/uu/numfmt/src/numeric.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// This file is part of the uutils coreutils package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
// This file is written to solve #11654
7+
// This mod is to preserve numeric precision for large integers.
8+
// GNU numfmt has a better precision on floats due to 'long double'.
9+
10+
#[derive(Clone, Copy, Debug, PartialEq)]
11+
pub(crate) enum ParsedNumber {
12+
ExactInt(i128),
13+
Float(f64),
14+
}
15+
16+
impl ParsedNumber {
17+
pub(crate) fn to_f64(self) -> f64 {
18+
match self {
19+
Self::ExactInt(n) => n as f64,
20+
Self::Float(n) => n,
21+
}
22+
}
23+
24+
pub(crate) fn exact_int(self) -> Option<i128> {
25+
match self {
26+
Self::ExactInt(n) => Some(n),
27+
Self::Float(_) => None,
28+
}
29+
}
30+
}

src/uu/numfmt/src/numfmt.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ use uucore::{format_usage, os_str_as_bytes, show, translate};
2929
pub mod errors;
3030
pub mod format;
3131
pub mod options;
32+
33+
mod numeric;
3234
mod units;
3335

3436
/// Format a single line and write it, handling `--invalid` error modes.

tests/by-util/test_numfmt.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1393,7 +1393,6 @@ fn test_negative_number_with_double_dash_gnu_compat_issue_11653() {
13931393
// https://github.com/uutils/coreutils/issues/11654
13941394
// uutils parses large integers through f64, losing precision past 2^53.
13951395
#[test]
1396-
#[ignore = "GNU compat: see uutils/coreutils#11654"]
13971396
fn test_large_integer_precision_loss_issue_11654() {
13981397
new_ucmd!()
13991398
.args(&["--from=iec", "9153396227555392131"])

0 commit comments

Comments
 (0)