From 58473db384dc1b193cd1e9bf486d357b5ddb365a Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Mon, 5 Oct 2026 21:31:43 +0200 Subject: [PATCH] fix: a time label's mantissa cannot reach its own radix `format_time()` picked its sub-second rung from the raw value, so three significant figures of a value just under a rung's top rounded to 1000 and `%.3g` wrote that as an exponent: a one-second tick computed as 0.9999999 s read `1e+03ms`. The rung is now chosen by the rounded mantissa, which is the same 999.5 check `format_si()` already made for the SI prefixes, and a value that rounds to a whole second leaves the sub-second branch entirely. --- src/plot/ticks.cpp | 11 +++++++++-- tests/test_ticks.cpp | 12 ++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/plot/ticks.cpp b/src/plot/ticks.cpp index f717878..93e72ec 100644 --- a/src/plot/ticks.cpp +++ b/src/plot/ticks.cpp @@ -98,7 +98,10 @@ void format_time(f64 seconds, char* buf, usize n) { const char* const sign = seconds < 0.0 ? "-" : ""; const f64 t = std::fabs(seconds); - if (t < 1.0) { + // `0.9995` and not `1.0`: three significant figures of 0.9999 s are `1e+03ms`, so a value + // that *rounds* to a second belongs to the whole-second branch below, which writes it `1s`. + // The same spill one rung down is handled inside the ladder. + if (t < 0.9995) { // Sub-second: the prefix that keeps the mantissa in [1, 1000) -- the same rule the SI // formatter uses on a y axis, and the reading a zoomed-in window needs. A fixed // millisecond unit printed `0.005ms` where `5us` is what the axis is showing, and @@ -119,8 +122,12 @@ void format_time(f64 seconds, char* buf, usize n) { {1.0e-9, "ns"}, {1.0e-12, "ps"}, }; + // The rung is picked by the *rounded* mantissa, which is what `format_si`'s own 999.5 + // check buys there: 0.9999 ms is `999.9µs` before rounding and `1e+03µs` after, so it is + // the millisecond rung's value and reads `1ms`. Testing the raw `t < u.scale` let the + // mantissa reach 1000 at every rung boundary. for (const auto& u : kUnits) { - if (t < u.scale) continue; + if (t < u.scale * 0.9995) continue; std::snprintf(buf, n, "%s%.3g%s", sign, t / u.scale, u.unit); return; } diff --git a/tests/test_ticks.cpp b/tests/test_ticks.cpp index ab6bdd8..d3ef63e 100644 --- a/tests/test_ticks.cpp +++ b/tests/test_ticks.cpp @@ -263,6 +263,18 @@ PLOT_TEST(format_time_never_lets_a_field_reach_its_radix) { plot::format_time(86399.5, b, sizeof b); PLOT_CHECK(std::string(b) == "1d"); plot::format_time(86400.0, b, sizeof b); PLOT_CHECK(std::string(b) == "1d"); + // A mantissa may not reach its own radix either: three significant figures of a value a + // hair under a rung's top round up to 1000, and `%.3g` writes that `1e+03`. A one-second + // tick computed as 0.9999999 s read `1e+03ms` on a live axis, which is where this was seen. + plot::format_time(0.9999999, b, sizeof b); PLOT_CHECK(std::string(b) == "1s"); + plot::format_time(0.9995, b, sizeof b); PLOT_CHECK(std::string(b) == "1s"); + plot::format_time(0.9994, b, sizeof b); PLOT_CHECK(std::string(b) == "999ms"); + plot::format_time(0.9999999e-3, b, sizeof b); PLOT_CHECK(std::string(b) == "1ms"); + plot::format_time(0.9994e-3, b, sizeof b); PLOT_CHECK(std::string(b) == "999\u00b5s"); + plot::format_time(0.9999999e-6, b, sizeof b); PLOT_CHECK(std::string(b) == "1\u00b5s"); + plot::format_time(0.9999999e-9, b, sizeof b); PLOT_CHECK(std::string(b) == "1ns"); + plot::format_time(-0.9999999, b, sizeof b); PLOT_CHECK(std::string(b) == "-1s"); + // Sweep the boundary neighbourhoods and check every emitted field. const double values[] = {0.4, 0.9995, 1.0, 59.4, 59.4999, 59.5, 59.9, 60.0, 90.0, 119.5, 3599.4, 3599.5, 3599.9, 86399.4, 86399.5, 86399.9, 86400.0, 172800.0, -119.5,