feat(reporter): allow stringifying exponent for counters in console (fixes #818) (#2305)

Adds benchmark::Counter::kStringifyExponent flag to format user counters with explicit exponents instead of SI prefixes in console output. Updates HumanReadableNumber to support stringifying the exponent (supporting *2^ for binary base 1024 and e for base 1000). Also updates Python bindings, user guide documentation, and test suite.

Co-authored-by: Roman Lebedev <lebedev.ri@gmail.com>
diff --git a/bindings/python/google_benchmark/benchmark.cc b/bindings/python/google_benchmark/benchmark.cc
index ccd7eb5..f60b0ce 100644
--- a/bindings/python/google_benchmark/benchmark.cc
+++ b/bindings/python/google_benchmark/benchmark.cc
@@ -122,6 +122,7 @@
              Counter::Flags::kIsIterationInvariantRate)
       .value("kAvgIterations", Counter::Flags::kAvgIterations)
       .value("kAvgIterationsRate", Counter::Flags::kAvgIterationsRate)
+      .value("kStringifyExponent", Counter::Flags::kStringifyExponent)
       .value("kInvert", Counter::Flags::kInvert)
       .export_values();
 
diff --git a/docs/user_guide.md b/docs/user_guide.md
index 9e46a51..2b6e800 100644
--- a/docs/user_guide.md
+++ b/docs/user_guide.md
@@ -1105,6 +1105,12 @@
 
   // This says that we process with the rate of state.range(0) bytes every iteration:
   state.counters["BytesProcessed"] = Counter(state.range(0), benchmark::Counter::kIsIterationInvariantRate, benchmark::Counter::OneK::kIs1024);
+
+  // Set the counter to stringify the exponent explicitly without SI prefixes (e.g. 1.2345e-04):
+  state.counters["Error"] = Counter(absError, benchmark::Counter::kStringifyExponent);
+
+  // Stringified exponents can also be combined with rates:
+  state.counters["ErrorRate"] = Counter(absError, benchmark::Counter::kStringifyExponent | benchmark::Counter::kIsRate);
 ```
 
 You can use `insert()` with `std::initializer_list`:
diff --git a/include/benchmark/counter.h b/include/benchmark/counter.h
index 8db5d8d..20fb368 100644
--- a/include/benchmark/counter.h
+++ b/include/benchmark/counter.h
@@ -39,6 +39,7 @@
     kIsIterationInvariantRate = kIsRate | kIsIterationInvariant,
     kAvgIterations = 1 << 3,
     kAvgIterationsRate = kIsRate | kAvgIterations,
+    kStringifyExponent = 1 << 4,
     kInvert = 1 << 31
   };
 
diff --git a/src/console_reporter.cc b/src/console_reporter.cc
index 84fe99d..7550434 100644
--- a/src/console_reporter.cc
+++ b/src/console_reporter.cc
@@ -192,7 +192,9 @@
       s = StrFormat("%.2f", 100. * c.second.value);
       unit = "%";
     } else {
-      s = HumanReadableNumber(c.second.value, c.second.oneK);
+      s = HumanReadableNumber(
+          c.second.value, c.second.oneK,
+          (c.second.flags & Counter::kStringifyExponent) != 0);
       if ((c.second.flags & Counter::kIsRate) != 0) {
         unit = (c.second.flags & Counter::kInvert) != 0 ? "s" : "/s";
       }
diff --git a/src/string_util.cc b/src/string_util.cc
index 9a0d542..8e70de8 100644
--- a/src/string_util.cc
+++ b/src/string_util.cc
@@ -109,9 +109,16 @@
 }
 
 std::string ToBinaryStringFullySpecified(double value, int precision,
-                                         Counter::OneK one_k) {
+                                         Counter::OneK one_k,
+                                         bool stringify_exponent = false) {
   auto [mantissa, exponent] = ToExponentAndMantissa(
       value, precision, one_k == Counter::kIs1024 ? 1024.0 : 1000.0);
+  if (stringify_exponent) {
+    const int64_t factor = (one_k == Counter::kIs1024 ? 10 : 3);
+    return StrFormat("%s%s%i", mantissa.c_str(),
+                     (one_k == Counter::kIs1024 ? "*2^" : "e"),
+                     static_cast<int>(exponent * factor));
+  }
   return mantissa + ExponentToPrefix(exponent, one_k == Counter::kIs1024);
 }
 
@@ -154,8 +161,9 @@
 
 }  // end namespace
 
-std::string HumanReadableNumber(double n, Counter::OneK one_k) {
-  return ToBinaryStringFullySpecified(n, 1, one_k);
+std::string HumanReadableNumber(double n, Counter::OneK one_k,
+                                bool stringify_exponent) {
+  return ToBinaryStringFullySpecified(n, 1, one_k, stringify_exponent);
 }
 
 std::string StrFormat(const char* format, ...) {
diff --git a/src/string_util.h b/src/string_util.h
index 1a84666..1c57fa2 100644
--- a/src/string_util.h
+++ b/src/string_util.h
@@ -13,7 +13,8 @@
 namespace benchmark {
 
 BENCHMARK_EXPORT
-std::string HumanReadableNumber(double n, Counter::OneK one_k);
+std::string HumanReadableNumber(double n, Counter::OneK one_k,
+                                bool stringify_exponent = false);
 
 BENCHMARK_EXPORT
 std::string StrFormat(const char* format, ...) PRINTF_FORMAT_STRING_FUNC(1, 2);
diff --git a/test/string_util_gtest.cc b/test/string_util_gtest.cc
index 5a9a09e..12eb2f1 100644
--- a/test/string_util_gtest.cc
+++ b/test/string_util_gtest.cc
@@ -200,4 +200,63 @@
   ASSERT_THAT(str, ::testing::MatchesRegex(std::get<2>(GetParam())));
 }
 
+using HumanReadableStringifyExponentFixture = ::testing::TestWithParam<
+    std::tuple<double, benchmark::Counter::OneK, std::string>>;
+
+INSTANTIATE_TEST_SUITE_P(
+    HumanReadableStringifyExponentTests, HumanReadableStringifyExponentFixture,
+    ::testing::Values(
+        // Base 1024 positive powers
+        std::make_tuple(0.0, benchmark::Counter::kIs1024, "0*2^0"),
+        std::make_tuple(999.0, benchmark::Counter::kIs1024, "999*2^0"),
+        std::make_tuple(1000.0, benchmark::Counter::kIs1024, "1000*2^0"),
+        std::make_tuple(1024.0, benchmark::Counter::kIs1024, "1*2^10"),
+        std::make_tuple(1000 * 1000.0, benchmark::Counter::kIs1024,
+                        "976.562*2^10"),
+        std::make_tuple(1024 * 1024.0, benchmark::Counter::kIs1024, "1*2^20"),
+        std::make_tuple(1000 * 1000 * 1000.0, benchmark::Counter::kIs1024,
+                        "953.674*2^20"),
+        std::make_tuple(1024 * 1024 * 1024.0, benchmark::Counter::kIs1024,
+                        "1*2^30"),
+        // Base 1024 negative powers
+        std::make_tuple(1.0 / 1024.0, benchmark::Counter::kIs1024, "1*2^-10"),
+        std::make_tuple(1.0 / (1024.0 * 1024.0), benchmark::Counter::kIs1024,
+                        "1*2^-20"),
+        std::make_tuple(1.0 / (1024.0 * 1024.0 * 1024.0),
+                        benchmark::Counter::kIs1024, "1*2^-30"),
+        std::make_tuple(1.0 / 1000.0, benchmark::Counter::kIs1024,
+                        "1.024*2^-10"),
+        std::make_tuple(1.0 / (1000.0 * 1000.0), benchmark::Counter::kIs1024,
+                        "1.04858*2^-20"),
+        std::make_tuple(1.0 / (1000.0 * 1000.0 * 1000.0),
+                        benchmark::Counter::kIs1024, "1.07374*2^-30"),
+        // Base 1000 positive powers
+        std::make_tuple(0.0, benchmark::Counter::kIs1000, "0e0"),
+        std::make_tuple(999.0, benchmark::Counter::kIs1000, "999e0"),
+        std::make_tuple(1000.0, benchmark::Counter::kIs1000, "1e3"),
+        std::make_tuple(1024.0, benchmark::Counter::kIs1000, "1.024e3"),
+        std::make_tuple(1000 * 1000.0, benchmark::Counter::kIs1000, "1e6"),
+        std::make_tuple(1024 * 1024.0, benchmark::Counter::kIs1000,
+                        "1.04858e6"),
+        std::make_tuple(1000 * 1000 * 1000.0, benchmark::Counter::kIs1000,
+                        "1e9"),
+        std::make_tuple(1024 * 1024 * 1024.0, benchmark::Counter::kIs1000,
+                        "1.07374e9"),
+        // Base 1000 negative powers
+        std::make_tuple(1e-3, benchmark::Counter::kIs1000, "1e-3"),
+        std::make_tuple(1e-6, benchmark::Counter::kIs1000, "1e-6"),
+        std::make_tuple(1e-9, benchmark::Counter::kIs1000, "1e-9"),
+        std::make_tuple(1.0 / 1024.0, benchmark::Counter::kIs1000,
+                        "976.562e-6"),
+        std::make_tuple(1.0 / (1024.0 * 1024.0), benchmark::Counter::kIs1000,
+                        "953.674e-9")));
+
+TEST_P(HumanReadableStringifyExponentFixture,
+       HumanReadableNumberStringifyExponent) {
+  std::string str = benchmark::HumanReadableNumber(std::get<0>(GetParam()),
+                                                   std::get<1>(GetParam()),
+                                                   /*stringify_exponent=*/true);
+  EXPECT_EQ(str, std::get<2>(GetParam()));
+}
+
 }  // end namespace
diff --git a/test/user_counters_test.cc b/test/user_counters_test.cc
index 9253d59..cdebd45 100644
--- a/test/user_counters_test.cc
+++ b/test/user_counters_test.cc
@@ -588,6 +588,58 @@
 }  // end namespace
 
 // ========================================================================= //
+// ----------------- Stringify Exponent Counters Output -------------------- //
+// ========================================================================= //
+
+namespace {
+void BM_Counters_kStringifyExponent(benchmark::State& state) {
+  for (auto _ : state) {
+    // This test requires a non-zero CPU time to avoid divide-by-zero
+    auto iterations = static_cast<double>(state.iterations()) *
+                      static_cast<double>(state.iterations());
+    benchmark::DoNotOptimize(iterations);
+  }
+  namespace bm = benchmark;
+  state.counters["foo"] =
+      bm::Counter{1.2345e-4, bm::Counter::kStringifyExponent};
+  state.counters["bar"] = bm::Counter{
+      2.3456e4, bm::Counter::kStringifyExponent | bm::Counter::kIsRate};
+}
+BENCHMARK(BM_Counters_kStringifyExponent);
+ADD_CASES(TC_ConsoleOut,
+          {{"^BM_Counters_kStringifyExponent "
+            "%console_report bar=[0-9]+([.][0-9]+)?e[+-]?[0-9]+/s "
+            "foo=[0-9]+([.][0-9]+)?e[+-]?[0-9]+$"}});
+ADD_CASES(TC_JSONOut,
+          {{"\"name\": \"BM_Counters_kStringifyExponent\",$"},
+           {"\"family_index\": 12,$", MR_Next},
+           {"\"per_family_instance_index\": 0,$", MR_Next},
+           {"\"run_name\": \"BM_Counters_kStringifyExponent\",$", MR_Next},
+           {"\"run_type\": \"iteration\",$", MR_Next},
+           {"\"repetitions\": 1,$", MR_Next},
+           {"\"repetition_index\": 0,$", MR_Next},
+           {"\"threads\": 1,$", MR_Next},
+           {"\"iterations\": %int,$", MR_Next},
+           {"\"real_time\": %float,$", MR_Next},
+           {"\"cpu_time\": %float,$", MR_Next},
+           {"\"time_unit\": \"ns\",$", MR_Next},
+           {"\"bar\": %float,$", MR_Next},
+           {"\"foo\": %float$", MR_Next},
+           {"}", MR_Next}});
+ADD_CASES(TC_CSVOut, {{"^\"BM_Counters_kStringifyExponent\",%csv_report,"
+                       "%float,%float$"}});
+// VS2013 does not allow this function to be passed as a lambda argument
+// to CHECK_BENCHMARK_RESULTS()
+void CheckStringifyExponent(Results const& e) {
+  double t = e.DurationCPUTime();
+  CHECK_FLOAT_COUNTER_VALUE(e, "foo", EQ, 1.2345e-4, 0.001);
+  CHECK_FLOAT_COUNTER_VALUE(e, "bar", EQ, 2.3456e4 / t, 0.001);
+}
+CHECK_BENCHMARK_RESULTS("BM_Counters_kStringifyExponent",
+                        &CheckStringifyExponent);
+}  // end namespace
+
+// ========================================================================= //
 // --------------------------- TEST CASES END ------------------------------ //
 // ========================================================================= //