Skip to content

Commit ac527f9

Browse files
authored
Merge pull request #8444 from drinkcat/seq-optimize-shortest
uucore: num_format: Optimize `format_float_shortest`
2 parents c053a40 + f7f3bbc commit ac527f9

File tree

1 file changed

+62
-36
lines changed

1 file changed

+62
-36
lines changed

src/uucore/src/lib/features/format/num_format.rs

Lines changed: 62 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,41 @@ fn format_float_decimal(
374374
format!("{bd:.precision$}")
375375
}
376376

377+
/// Converts a `&BigDecimal` to a scientific-like `X.XX * 10^e`.
378+
/// - The returned `String` contains the digits `XXX`, _without_ the separating
379+
/// `.` (the caller must add that to get a valid scientific format number).
380+
/// - `e` is an integer exponent.
381+
fn bd_to_string_exp_with_prec(bd: &BigDecimal, precision: usize) -> (String, i64) {
382+
// TODO: A lot of time is spent in `with_prec` computing the exact number
383+
// of digits, it might be possible to save computation time by doing a rough
384+
// division followed by arithmetics on `digits` to round if necessary (using
385+
// `fast_inc`).
386+
387+
// Round bd to precision digits (including the leading digit)
388+
// Note that `with_prec` will produce an extra digit if rounding overflows
389+
// (e.g. 9995.with_prec(3) => 1000 * 10^1, but we want 100 * 10^2), we compensate
390+
// for that later.
391+
let bd_round = bd.with_prec(precision as u64);
392+
393+
// Convert to the form XXX * 10^-p (XXX is precision digit long)
394+
let (frac, mut p) = bd_round.as_bigint_and_exponent();
395+
396+
let mut digits = frac.to_str_radix(10);
397+
398+
// In the unlikely case we had an overflow, correct for that.
399+
if digits.len() == precision + 1 {
400+
debug_assert!(&digits[precision..] == "0");
401+
digits.truncate(precision);
402+
p -= 1;
403+
}
404+
405+
// If we end up with scientific formatting, we would convert XXX to X.XX:
406+
// that divides by 10^(precision-1), so add that to the exponent.
407+
let exponent = -p + precision as i64 - 1;
408+
409+
(digits, exponent)
410+
}
411+
377412
fn format_float_scientific(
378413
bd: &BigDecimal,
379414
precision: Option<usize>,
@@ -395,20 +430,10 @@ fn format_float_scientific(
395430
};
396431
}
397432

398-
// Round bd to (1 + precision) digits (including the leading digit)
399-
// We call `with_prec` twice as it will produce an extra digit if rounding overflows
400-
// (e.g. 9995.with_prec(3) => 1000 * 10^1, but we want 100 * 10^2).
401-
let bd_round = bd
402-
.with_prec(precision as u64 + 1)
403-
.with_prec(precision as u64 + 1);
404-
405-
// Convert to the form XXX * 10^-e (XXX is 1+precision digit long)
406-
let (frac, e) = bd_round.as_bigint_and_exponent();
433+
let (digits, exponent) = bd_to_string_exp_with_prec(bd, precision + 1);
407434

408-
// Scale down "XXX" to "X.XX": that divides by 10^precision, so add that to the exponent.
409-
let digits = frac.to_str_radix(10);
435+
// TODO: Optimizations in format_float_shortest can be made here as well
410436
let (first_digit, remaining_digits) = digits.split_at(1);
411-
let exponent = -e + precision as i64;
412437

413438
let dot =
414439
if !remaining_digits.is_empty() || (precision == 0 && ForceDecimal::Yes == force_decimal) {
@@ -445,18 +470,8 @@ fn format_float_shortest(
445470
};
446471
}
447472

448-
// Round bd to precision digits (including the leading digit)
449-
// We call `with_prec` twice as it will produce an extra digit if rounding overflows
450-
// (e.g. 9995.with_prec(3) => 1000 * 10^1, but we want 100 * 10^2).
451-
let bd_round = bd.with_prec(precision as u64).with_prec(precision as u64);
452-
453-
// Convert to the form XXX * 10^-p (XXX is precision digit long)
454-
let (frac, e) = bd_round.as_bigint_and_exponent();
455-
456-
let digits = frac.to_str_radix(10);
457-
// If we end up with scientific formatting, we would convert XXX to X.XX:
458-
// that divides by 10^(precision-1), so add that to the exponent.
459-
let exponent = -e + precision as i64 - 1;
473+
let mut output = String::with_capacity(precision);
474+
let (digits, exponent) = bd_to_string_exp_with_prec(bd, precision);
460475

461476
if exponent < -4 || exponent >= precision as i64 {
462477
// Scientific-ish notation (with a few differences)
@@ -465,42 +480,53 @@ fn format_float_shortest(
465480
let (first_digit, remaining_digits) = digits.split_at(1);
466481

467482
// Always add the dot, we might trim it later.
468-
let mut normalized = format!("{first_digit}.{remaining_digits}");
483+
output.push_str(first_digit);
484+
output.push('.');
485+
output.push_str(remaining_digits);
469486

470487
if force_decimal == ForceDecimal::No {
471-
strip_fractional_zeroes_and_dot(&mut normalized);
488+
strip_fractional_zeroes_and_dot(&mut output);
472489
}
473490

474-
let exp_char = match case {
491+
output.push(match case {
475492
Case::Lowercase => 'e',
476493
Case::Uppercase => 'E',
477-
};
494+
});
478495

479-
format!("{normalized}{exp_char}{exponent:+03}")
496+
// Format the exponent
497+
let exponent_abs = exponent.abs();
498+
output.push(if exponent < 0 { '-' } else { '+' });
499+
if exponent_abs < 10 {
500+
output.push('0');
501+
}
502+
output.push_str(&exponent_abs.to_string());
480503
} else {
481504
// Decimal-ish notation with a few differences:
482505
// - The precision works differently and specifies the total number
483506
// of digits instead of the digits in the fractional part.
484507
// - If we don't force the decimal, `.` and trailing `0` in the fractional part
485508
// are trimmed.
486-
let mut formatted = if exponent < 0 {
509+
if exponent < 0 {
487510
// Small number, prepend some "0.00" string
488-
let zeros = "0".repeat(-exponent as usize - 1);
489-
format!("0.{zeros}{digits}")
511+
output.push_str("0.");
512+
output.extend(std::iter::repeat_n('0', -exponent as usize - 1));
513+
output.push_str(&digits);
490514
} else {
491515
// exponent >= 0, slot in a dot at the right spot
492516
let (first_digits, remaining_digits) = digits.split_at(exponent as usize + 1);
493517

494518
// Always add `.` even if it's trailing, we might trim it later
495-
format!("{first_digits}.{remaining_digits}")
519+
output.push_str(first_digits);
520+
output.push('.');
521+
output.push_str(remaining_digits);
496522
};
497523

498524
if force_decimal == ForceDecimal::No {
499-
strip_fractional_zeroes_and_dot(&mut formatted);
525+
strip_fractional_zeroes_and_dot(&mut output);
500526
}
501-
502-
formatted
503527
}
528+
529+
output
504530
}
505531

506532
fn format_float_hexadecimal(

0 commit comments

Comments
 (0)