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