File 0001-fix-util-implement-time_with_unit-using-integer-arit.patch of Package dwarfs

From 0c870a73aa8a4a37ae0e6fc02605a482740d5ecb Mon Sep 17 00:00:00 2001
From: Marcus Holland-Moritz <github@mhxnet.de>
Date: Tue, 24 Mar 2026 09:46:08 +0100
Subject: [PATCH 1/4] fix(util): implement `time_with_unit` using integer
 arithmetic (gh #354)

---
 src/util.cpp        | 152 +++++++++++++++++++++++++++++++-------------
 test/utils_test.cpp |   8 +++
 2 files changed, 115 insertions(+), 45 deletions(-)

diff --git a/src/util.cpp b/src/util.cpp
index 18fab13a..a32ec8f6 100644
--- a/src/util.cpp
+++ b/src/util.cpp
@@ -29,9 +29,11 @@
 #include <algorithm>
 #include <array>
 #include <bit>
+#include <cassert>
 #include <charconv>
 #include <clocale>
 #include <csignal>
+#include <cstdint>
 #include <cstdio>
 #include <cstdlib>
 #include <fstream>
@@ -224,62 +226,122 @@ std::string size_with_unit(file_size_t const size) {
 }
 
 std::string time_with_unit(double const sec) {
+  return time_with_unit(
+      std::chrono::nanoseconds(static_cast<int64_t>(sec * 1e9)));
+}
+
+std::string time_with_unit(std::chrono::nanoseconds const ns) {
   static constexpr int kPrecision = 4;
 
-  assert(sec >= 0.0);
+  assert(ns.count() >= 0);
 
-  auto truncate_to_decimals = [](double value, int decimals) {
-    auto const factor = std::pow(10.0, decimals);
-    return std::trunc(value * factor) / factor;
+  auto num_digits = [](uint64_t n) {
+    int digits = 0;
+    while (n > 0) {
+      n /= 10;
+      ++digits;
+    }
+    return digits;
   };
 
-  std::string result;
+  auto pow10 = [](unsigned n) -> uint64_t {
+    uint64_t v = 1;
+    while (n-- > 0) {
+      v *= 10;
+    }
+    return v;
+  };
 
-  if (sec < 60.0) {
-    static constexpr std::array units{"s", "ms", "us", "ns"};
-    auto val = sec;
-    int mag = 0;
+  auto format_decimal = [&](uint64_t whole, uint64_t frac, unsigned frac_digits,
+                            std::string_view suffix) {
+    std::string out = fmt::format("{}", whole);
 
-    while (val < 1.0 && std::cmp_less(mag, units.size())) {
-      val *= 1000.0;
-      ++mag;
+    if (frac_digits > 0 && frac > 0) {
+      std::string frac_str(frac_digits, '0');
+      for (unsigned i = 0; i < frac_digits; ++i) {
+        frac_str[frac_digits - 1 - i] = static_cast<char>('0' + (frac % 10));
+        frac /= 10;
+      }
+
+      while (!frac_str.empty() && frac_str.back() == '0') {
+        frac_str.pop_back();
+      }
+
+      if (!frac_str.empty()) {
+        out += '.';
+        out += frac_str;
+      }
     }
 
-    if (std::cmp_less(mag, units.size())) {
-      val = truncate_to_decimals(val, kPrecision - std::ceil(std::log10(val)));
-      result = fmt::format("{:.{}g}{}", val, kPrecision, units[mag]);
-    } else {
-      result = "0s";
+    out += suffix;
+    return out;
+  };
+
+  auto format_truncated = [&](uint64_t value, uint64_t scale,
+                              unsigned frac_digits, std::string_view suffix) {
+    auto const whole = value / scale;
+    if (frac_digits == 0) {
+      return fmt::format("{}{}", whole, suffix);
     }
 
-    return result;
+    auto const factor = pow10(frac_digits);
+    auto const frac = (value % scale) * factor / scale; // truncation
+    return format_decimal(whole, frac, frac_digits, suffix);
+  };
+
+  auto const total_ns = static_cast<uint64_t>(ns.count());
+
+  if (total_ns == 0) {
+    return "0s";
   }
 
-  struct unit_spec {
-    int scale;
-    std::string_view suffix;
-  };
+  // Sub-minute formatting: choose one unit and show up to 4 significant digits,
+  // truncating rather than rounding.
+  if (ns < std::chrono::minutes(1)) {
+    struct short_unit_spec {
+      uint64_t scale_ns;
+      std::string_view suffix;
+    };
 
-  static constexpr std::array units{
-      unit_spec{86400, "d"},
-      unit_spec{3600, "h"},
-      unit_spec{60, "m"},
-  };
+    static constexpr std::array short_units{
+        short_unit_spec{1'000'000'000ULL, "s"},
+        short_unit_spec{1'000'000ULL, "ms"},
+        short_unit_spec{1'000ULL, "us"},
+        short_unit_spec{1ULL, "ns"},
+    };
 
-  auto num_digits = [](int n) {
-    int digits = 0;
-    while (n > 0) {
-      n /= 10;
-      ++digits;
+    for (auto const& [scale_ns, suffix] : short_units) {
+      if (total_ns >= scale_ns) {
+        auto const whole = total_ns / scale_ns;
+        auto const digits = num_digits(whole);
+        auto const frac_digits =
+            static_cast<unsigned>(std::max(0, kPrecision - digits));
+        return format_truncated(total_ns, scale_ns, frac_digits, suffix);
+      }
     }
-    return digits;
+
+    return "0s";
+  }
+
+  // Minute-and-up formatting: spend a 4-digit budget across d/h/m, then show
+  // seconds with truncated decimals if there is budget left.
+  struct long_unit_spec {
+    uint64_t scale_ns;
+    std::string_view suffix;
+  };
+
+  static constexpr std::array long_units{
+      long_unit_spec{86'400ULL * 1'000'000'000ULL, "d"},
+      long_unit_spec{3'600ULL * 1'000'000'000ULL, "h"},
+      long_unit_spec{60ULL * 1'000'000'000ULL, "m"},
   };
 
-  double remainder = sec;
+  std::string result;
+  uint64_t remainder = total_ns;
   int rem_digits = kPrecision;
 
-  for (auto const& [scale, suffix] : units) {
-    auto const value = static_cast<int>(remainder / scale);
+  for (auto const& [scale_ns, suffix] : long_units) {
+    auto const value = remainder / scale_ns;
     auto const digits = result.empty() ? num_digits(value) : 2;
 
     if (value > 0) {
@@ -290,7 +352,7 @@ std::string time_with_unit(double const sec) {
     }
 
     rem_digits -= digits;
-    remainder -= value * scale;
+    remainder -= value * scale_ns;
 
     if (rem_digits <= 0) {
       break;
@@ -298,11 +360,15 @@ std::string time_with_unit(double const sec) {
   }
 
   if (rem_digits > 0) {
+    auto const frac_digits = static_cast<unsigned>(std::max(0, rem_digits - 2));
     auto const seconds =
-        truncate_to_decimals(remainder, std::max(0, rem_digits - 2));
-    if (seconds > 0.0) {
-      fmt::format_to(std::back_inserter(result), " {:.{}g}s", seconds,
-                     kPrecision);
+        format_truncated(remainder, 1'000'000'000ULL, frac_digits, "s");
+
+    if (seconds != "0s") {
+      if (!result.empty()) {
+        result += ' ';
+      }
+      result += seconds;
     }
   }
 
@@ -311,10 +377,6 @@ std::string time_with_unit(double const sec) {
   return result;
 }
 
-std::string time_with_unit(std::chrono::nanoseconds ns) {
-  return time_with_unit(1e-9 * ns.count());
-}
-
 file_size_t parse_size_with_unit(std::string const& str) {
   file_size_t value;
   auto [ptr, ec]{std::from_chars(str.data(), str.data() + str.size(), value)};
diff --git a/test/utils_test.cpp b/test/utils_test.cpp
index 00b2c7e0..d74a27b2 100644
--- a/test/utils_test.cpp
+++ b/test/utils_test.cpp
@@ -500,16 +500,24 @@ TEST(utils, size_with_unit) {
 
 TEST(utils, time_with_unit) {
   using namespace std::chrono_literals;
+
   EXPECT_EQ("0s", time_with_unit(0ms));
+  EXPECT_EQ("25ns", time_with_unit(25ns));
+  EXPECT_EQ("40ns", time_with_unit(40ns));
+  EXPECT_EQ("999.9us", time_with_unit(999999ns));
   EXPECT_EQ("999ms", time_with_unit(999ms));
+  EXPECT_EQ("999.9ms", time_with_unit(999999us));
+  EXPECT_EQ("999.9ms", time_with_unit(999900us));
   EXPECT_EQ("1s", time_with_unit(1000ms));
   EXPECT_EQ("1.5s", time_with_unit(1500ms));
+  EXPECT_EQ("1.001s", time_with_unit(1001ms));
   EXPECT_EQ("59s", time_with_unit(59s));
   EXPECT_EQ("1m", time_with_unit(60s));
   EXPECT_EQ("1m 1s", time_with_unit(61s));
   EXPECT_EQ("1m 45s", time_with_unit(105s));
   EXPECT_EQ("12.5us", time_with_unit(12500ns));
   EXPECT_EQ("1h 2m 3s", time_with_unit(1h + 2min + 3s));
+  EXPECT_EQ("1m 0.5s", time_with_unit(1min + 500ms));
   EXPECT_EQ("1m 1s", time_with_unit(1min + 1000ms));
   EXPECT_EQ("1m 1.5s", time_with_unit(1min + 1530ms));
   EXPECT_EQ("1m 1.5s", time_with_unit(1min + 1578ms));
-- 
2.53.0

openSUSE Build Service is sponsored by