File 0002-fix-utils-implement-ratio_to_string-using-integer-ar.patch of Package dwarfs
From e2520e5aa6571d3db96d46b4800dd8c67191197a Mon Sep 17 00:00:00 2001
From: Marcus Holland-Moritz <github@mhxnet.de>
Date: Tue, 24 Mar 2026 14:55:58 +0100
Subject: [PATCH 2/4] fix(utils): implement `ratio_to_string` using integer
arithmetic (gh #354)
---
include/dwarfs/util.h | 4 +-
src/util.cpp | 225 ++++++++++++++++++++++++++++++++++++++----
test/utils_test.cpp | 33 ++++++-
3 files changed, 239 insertions(+), 23 deletions(-)
diff --git a/include/dwarfs/util.h b/include/dwarfs/util.h
index ed48fbc2..d3cbdab0 100644
--- a/include/dwarfs/util.h
+++ b/include/dwarfs/util.h
@@ -30,6 +30,7 @@
#include <chrono>
#include <cstddef>
+#include <cstdint>
#include <exception>
#include <filesystem>
#include <iosfwd>
@@ -49,7 +50,8 @@ class os_access;
std::string time_with_unit(double sec);
std::string time_with_unit(std::chrono::nanoseconds ns);
std::string size_with_unit(file_size_t size);
-std::string ratio_to_string(double num, double den, int precision = 3);
+std::string
+ratio_to_string(std::uint64_t num, std::uint64_t den, int precision = 3);
file_size_t parse_size_with_unit(std::string const& str);
std::chrono::nanoseconds parse_time_with_unit(std::string const& str);
std::chrono::system_clock::time_point parse_time_point(std::string const& str);
diff --git a/src/util.cpp b/src/util.cpp
index a32ec8f6..fff13415 100644
--- a/src/util.cpp
+++ b/src/util.cpp
@@ -42,6 +42,7 @@
#include <optional>
#include <sstream>
#include <type_traits>
+#include <utility>
#include <vector>
#include <fmt/format.h>
@@ -210,6 +211,185 @@ void get_self_memory_usage_linux(memory_usage_mode const mode,
}
#endif
+struct rounded_decimal {
+ // value = digits * 10^exp10
+ // digits has no leading zeros and no trailing zeros, except "0"
+ std::string digits;
+ int exp10{0};
+};
+
+struct exact_digits {
+ // First N significant decimal digits of the exact value, without decimal
+ // point. scientific exponent of the first digit:
+ // value = 0.digits... * 10^(sci_exp + 1)
+ // = digits[0].digits[1]... * 10^sci_exp
+ std::string digits;
+ int sci_exp{0};
+};
+
+void increment_decimal_string(std::string& s) {
+ for (std::size_t i = s.size(); i-- > 0;) {
+ if (s[i] != '9') {
+ ++s[i];
+ return;
+ }
+ s[i] = '0';
+ }
+ s.insert(s.begin(), '1');
+}
+
+// Computes:
+// q = floor(rem * 10 / den)
+// r = (rem * 10) % den
+std::pair<unsigned, std::uint64_t>
+mul10_div(std::uint64_t rem, std::uint64_t den) {
+ unsigned q = 0;
+ std::uint64_t acc = 0;
+
+ assert(den != 0);
+ assert(rem < den);
+
+ for (int i = 0; i < 10; ++i) {
+ // Since acc < den and rem < den, this computes:
+ if (acc >= den - rem) {
+ acc = acc - (den - rem);
+ ++q;
+ } else {
+ acc += rem;
+ }
+ }
+
+ return {q, acc};
+}
+
+exact_digits
+extract_significant_digits(std::uint64_t num, std::uint64_t den, int count) {
+ if (num == 0) {
+ return {"0", 0};
+ }
+
+ std::string digits;
+ digits.reserve(static_cast<std::size_t>(count));
+
+ int sci_exp;
+ std::uint64_t rem;
+
+ auto append_next_digit = [&] {
+ auto const [d, next_rem] = mul10_div(rem, den);
+ rem = next_rem;
+ digits.push_back(static_cast<char>('0' + d));
+ return d;
+ };
+
+ if (num >= den) {
+ auto const integer_part = num / den;
+ rem = num % den;
+
+ digits = std::to_string(integer_part);
+ sci_exp = static_cast<int>(digits.size()) - 1;
+
+ if (std::cmp_greater(digits.size(), count)) {
+ digits.resize(static_cast<std::size_t>(count));
+ }
+ } else {
+ rem = num;
+ sci_exp = -1;
+
+ while (append_next_digit() == 0) {
+ digits.pop_back();
+ --sci_exp;
+ }
+ }
+
+ while (std::cmp_less(digits.size(), count)) {
+ append_next_digit();
+ }
+
+ return {std::move(digits), sci_exp};
+}
+
+rounded_decimal
+round_to_significant(exact_digits const& x, int precision, int shift10) {
+ if (x.digits == "0") {
+ return {"0", 0};
+ }
+
+ auto sig = x.digits.substr(0, precision);
+ int const guard = x.digits[static_cast<std::size_t>(precision)] - '0';
+
+ if (guard >= 5) {
+ increment_decimal_string(sig);
+ }
+
+ // keep the scale corresponding to exactly `precision` significant digits
+ int exp10 = x.sci_exp + shift10 - (precision - 1);
+
+ while (sig.size() > 1 && sig.back() == '0') {
+ sig.pop_back();
+ ++exp10;
+ }
+
+ return {std::move(sig), exp10};
+}
+
+int scientific_exponent(rounded_decimal const& x) {
+ if (x.digits == "0") {
+ return 0;
+ }
+ return x.exp10 + static_cast<int>(x.digits.size()) - 1;
+}
+
+bool in_range_for_percent(rounded_decimal const& x) {
+ int const e = scientific_exponent(x);
+ return e >= -1 && e < 2; // [1e-1, 1e2)
+}
+
+bool in_range_for_ppm_or_ppb(rounded_decimal const& x) {
+ int const e = scientific_exponent(x);
+ return e >= 0 && e < 3; // [1, 1000)
+}
+
+std::string to_fixed_string(rounded_decimal const& x) {
+ if (x.digits == "0") {
+ return "0";
+ }
+
+ std::string s = x.digits;
+
+ if (x.exp10 >= 0) {
+ s.append(static_cast<std::size_t>(x.exp10), '0');
+ return s;
+ }
+
+ auto const point = static_cast<int>(s.size()) + x.exp10;
+
+ if (point > 0) {
+ s.insert(static_cast<std::size_t>(point), 1, '.');
+ return s;
+ }
+
+ return "0." + std::string(static_cast<std::size_t>(-point), '0') + s;
+}
+
+std::string to_scientific_string(rounded_decimal const& x) {
+ if (x.digits == "0") {
+ return "0";
+ }
+
+ auto const e = scientific_exponent(x);
+
+ if (x.digits.size() == 1) {
+ return x.digits + "e" + std::to_string(e);
+ }
+
+ return x.digits.substr(0, 1) + "." + x.digits.substr(1) + "e" +
+ std::to_string(e);
+}
+
+bool less_than_one_thousandth(rounded_decimal const& x) {
+ return scientific_exponent(x) < -3;
+}
+
} // namespace
std::string size_with_unit(file_size_t const size) {
@@ -417,35 +597,44 @@ file_size_t parse_size_with_unit(std::string const& str) {
DWARFS_THROW(runtime_error, fmt::format("unsupported size suffix: {}", ptr));
}
-std::string ratio_to_string(double num, double den, int precision) {
- DWARFS_PUSH_WARNING
- DWARFS_GNU_DISABLE_WARNING("-Wfloat-equal")
+std::string ratio_to_string(std::uint64_t const num, std::uint64_t const den,
+ int const precision) {
+ assert(precision > 0);
- if (den == 0.0) {
+ if (den == 0) {
return "N/A";
}
- if (num == 0.0) {
+ if (num == 0) {
return "0x";
}
- DWARFS_POP_WARNING
+ // We only ever need the first `precision + 1` significant digits of the
+ // exact value. Multiplying by %, ppm, or ppb just shifts the decimal point.
+ exact_digits const base = extract_significant_digits(num, den, precision + 1);
- double const ratio = num / den;
+ if (auto const percent = round_to_significant(base, precision, 2);
+ in_range_for_percent(percent)) {
+ return to_fixed_string(percent) + "%";
+ }
- if (ratio < 1.0) {
- if (ratio >= 1e-3) {
- return fmt::format("{:.{}g}%", ratio * 100.0, precision);
- }
- if (ratio >= 1e-6) {
- return fmt::format("{:.{}g}ppm", ratio * 1e6, precision);
- }
- if (ratio >= 1e-9) {
- return fmt::format("{:.{}g}ppb", ratio * 1e9, precision);
- }
+ if (auto const ppm = round_to_significant(base, precision, 6);
+ in_range_for_ppm_or_ppb(ppm)) {
+ return to_fixed_string(ppm) + "ppm";
+ }
+
+ if (auto const ppb = round_to_significant(base, precision, 9);
+ in_range_for_ppm_or_ppb(ppb)) {
+ return to_fixed_string(ppb) + "ppb";
+ }
+
+ auto const plain = round_to_significant(base, precision, 0);
+
+ if (less_than_one_thousandth(plain)) {
+ return to_scientific_string(plain) + "x";
}
- return fmt::format("{:.{}g}x", ratio, precision);
+ return to_fixed_string(plain) + "x";
}
std::chrono::nanoseconds parse_time_with_unit(std::string const& str) {
diff --git a/test/utils_test.cpp b/test/utils_test.cpp
index d74a27b2..1f48494b 100644
--- a/test/utils_test.cpp
+++ b/test/utils_test.cpp
@@ -598,12 +598,13 @@ TEST(utils, time_with_unit) {
}
TEST(utils, ratio_to_string) {
+ EXPECT_EQ("N/A", ratio_to_string(1, 0));
EXPECT_EQ("0x", ratio_to_string(0, 1));
EXPECT_EQ("1x", ratio_to_string(1, 1));
EXPECT_EQ("1.5x", ratio_to_string(3, 2));
- EXPECT_EQ("10.7x", ratio_to_string(10.744, 1));
- EXPECT_EQ("11x", ratio_to_string(10.744, 1, 2));
- EXPECT_EQ("10.74x", ratio_to_string(10.744, 1, 4));
+ EXPECT_EQ("10.7x", ratio_to_string(10744, 1000));
+ EXPECT_EQ("11x", ratio_to_string(10744, 1000, 2));
+ EXPECT_EQ("10.74x", ratio_to_string(10744, 1000, 4));
EXPECT_EQ("99.9%", ratio_to_string(999, 1000));
EXPECT_EQ("0.1%", ratio_to_string(1, 1000));
EXPECT_EQ("999ppm", ratio_to_string(999, 1'000'000));
@@ -612,7 +613,31 @@ TEST(utils, ratio_to_string) {
EXPECT_EQ("10.7ppm", ratio_to_string(10'744, 1'000'000'000));
EXPECT_EQ("999ppb", ratio_to_string(999, 1'000'000'000));
EXPECT_EQ("1ppb", ratio_to_string(1, 1'000'000'000));
- EXPECT_EQ("1.78e-12x", ratio_to_string(1.7777, 1'000'000'000'000));
+ EXPECT_EQ("1.78e-12x", ratio_to_string(17777, 10'000'000'000'000'000));
+ EXPECT_EQ("1ppm", ratio_to_string(9996, 10'000'000'000));
+ EXPECT_EQ("999.6ppb", ratio_to_string(9996, 10'000'000'000, 4));
+ EXPECT_EQ("999ppm", ratio_to_string(9994, 10'000'000, 3));
+ EXPECT_EQ("0.1%", ratio_to_string(9996, 10'000'000, 3));
+ EXPECT_EQ("999.6ppm", ratio_to_string(9996, 10'000'000, 4));
+ EXPECT_EQ("999ppb", ratio_to_string(9994, 10'000'000'000, 3));
+ EXPECT_EQ("1ppm", ratio_to_string(9996, 10'000'000'000, 3));
+ EXPECT_EQ("999.6ppb", ratio_to_string(9996, 10'000'000'000, 4));
+ EXPECT_EQ("99.9%", ratio_to_string(9994, 10'000, 3));
+ EXPECT_EQ("1x", ratio_to_string(9996, 10'000, 3));
+ EXPECT_EQ("99.96%", ratio_to_string(9996, 10'000, 4));
+ EXPECT_EQ("0.1%", ratio_to_string(9995, 10'000'000, 3));
+ EXPECT_EQ("1ppm", ratio_to_string(9995, 10'000'000'000, 3));
+ EXPECT_EQ("1x", ratio_to_string(9995, 10'000, 3));
+ EXPECT_EQ("0.1%", ratio_to_string(1, 1'000, 3));
+ EXPECT_EQ("1ppm", ratio_to_string(1, 1'000'000, 3));
+ EXPECT_EQ("1ppb", ratio_to_string(1, 1'000'000'000, 3));
+ EXPECT_EQ("999.5ppm", ratio_to_string(9'995, 10'000'000, 4));
+ EXPECT_EQ("99.95%", ratio_to_string(9'995, 10'000, 4));
+ EXPECT_EQ("5e-10x", ratio_to_string(5, 10'000'000'000, 1));
+ EXPECT_EQ("2x", ratio_to_string(15, 10, 1));
+ EXPECT_EQ("0.1%", ratio_to_string(1, 1'000));
+ EXPECT_EQ("1ppm", ratio_to_string(1, 1'000'000));
+ EXPECT_EQ("1ppb", ratio_to_string(1, 1'000'000'000));
}
TEST(utils, basename) {
--
2.53.0