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

openSUSE Build Service is sponsored by