Skip to content

[core] Utility to round number and error in sync with 1 or 2 digits #18691

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 1 commit into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/foundation/inc/ROOT/StringUtils.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ std::string Join(const std::string &sep, StringCollection_t &&strings)
[&sep](auto const &a, auto const &b) { return a + sep + b; });
}

std::string Round(double value, double error, unsigned int cutoff = 1, std::string_view delim = "#pm");

} // namespace ROOT

#endif
64 changes: 64 additions & 0 deletions core/foundation/src/StringUtils.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
*************************************************************************/

#include "ROOT/StringUtils.hxx"
#include <sstream>
#include <cmath>
#include <ios>
#include <cassert>

namespace ROOT {

Expand All @@ -37,4 +41,64 @@ std::vector<std::string> Split(std::string_view str, std::string_view delims, bo
return out;
}

/**
* \brief Convert (round) a value and its uncertainty to string using one or two significant digits of the error
* \param error the error. If the error is negative or zero, only the value is returned with no specific rounding
* applied, using std::to_string
* \param cutoff should lay between 0 and 9. If first significant digit of error starts with value <= cutoff,
* use two significant digits instead of two for rounding. Set this value to zero to always use a
* single digit; set this value to 9 to always use two digits.
* \param delim delimiter between value and error printed into returned string, leave default for using ROOT's latex
* mode.
* \return a string with printed rounded value and error separated by "+/-" in ROOT latex mode \note The return
* format is `A+-B` using ios::fixed with the proper precision; for very large or very small values of the error, the
* format is changed from `A+-B` to (A'+-B')*1eX, with X being multiple of 3, respecting the corresponding precision.
* \see https://www.bipm.org/en/doi/10.59161/jcgm100-2008e, https://physics.nist.gov/cuu/Uncertainty/
*/
std::string Round(double value, double error, unsigned int cutoff, std::string_view delim)
{
if (error <= 0.) {
return std::to_string(value);
}

int error_exponent_base10_rounded = std::floor(std::log10(error));
const auto error_magnitude_base10 = std::pow(10., error_exponent_base10_rounded);
const auto error_first_digit = static_cast<unsigned int>(error / error_magnitude_base10);
assert(error_first_digit > 0 && error_first_digit < 10);
if (error_first_digit <= cutoff) {
const double rescaled_error = error * std::pow(10., -1. * error_exponent_base10_rounded);
if (static_cast<unsigned int>(std::round(rescaled_error * 10) / 10) <= cutoff)
error_exponent_base10_rounded--;
} else if (cutoff == 0 && error_first_digit == 9) {
const double rounded_rescaled_error = std::round(error * std::pow(10., -1. * error_exponent_base10_rounded));
const int rounded_rescaled_error_exponent_base10_rounded = std::floor(std::log10(rounded_rescaled_error));
const auto rounded_rescaled_error_magnitude_base10 =
std::pow(10., rounded_rescaled_error_exponent_base10_rounded);
const auto rounded_rescaled_error_first_digit =
static_cast<int>(rounded_rescaled_error / rounded_rescaled_error_magnitude_base10);
if (rounded_rescaled_error_first_digit == 1)
error_exponent_base10_rounded++;
}
const int factored_out_exponent_base10 = error <= 1e-3 ? static_cast<int>(std::floor(std::log10(error) / 3)) * 3
: static_cast<int>(std::log10(error) / 3) * 3;

std::stringstream result;
result.setf(std::ios::fixed);
if (error_exponent_base10_rounded - factored_out_exponent_base10 < 0) {
result.precision(-error_exponent_base10_rounded + factored_out_exponent_base10);
} else {
result.precision(0);
}
if (factored_out_exponent_base10 != 0)
result << "(";
result << std::round(value * std::pow(10., -error_exponent_base10_rounded)) /
std::pow(10., -error_exponent_base10_rounded + factored_out_exponent_base10);
result << delim;
result << std::round(error * std::pow(10., -error_exponent_base10_rounded)) /
std::pow(10., -error_exponent_base10_rounded + factored_out_exponent_base10);
if (factored_out_exponent_base10 != 0)
result << ")*1e" << factored_out_exponent_base10;
return result.str();
}

} // namespace ROOT
40 changes: 40 additions & 0 deletions core/foundation/test/testStringUtils.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,43 @@ TEST(StringUtils, Join)
test("", "", {});
test("", ";;", {""});
}

TEST(StringUtils, Round)
{
EXPECT_EQ(ROOT::Round(0.000000014, 0.000000024), "(10#pm20)*1e-9");
EXPECT_EQ(ROOT::Round(0.000000014, 0.000000018), "(14#pm18)*1e-9");
EXPECT_EQ(ROOT::Round(110., 0.24), "110.0#pm0.2");
EXPECT_EQ(ROOT::Round(120., 0.24, 2), "120.00#pm0.24");
EXPECT_EQ(ROOT::Round(130., 0.94), "130.0#pm0.9");
EXPECT_EQ(ROOT::Round(-140., 0.95), "-140.0#pm1.0");
EXPECT_EQ(ROOT::Round(150., 0.114), "150.00#pm0.11");
EXPECT_EQ(ROOT::Round(-160., 0.194), "-160.00#pm0.19");
EXPECT_EQ(ROOT::Round(170., 0.195), "170.0#pm0.2");
EXPECT_EQ(ROOT::Round(-180., 0.94), "-180.0#pm0.9");
EXPECT_EQ(ROOT::Round(190., 0.95), "190.0#pm1.0");
EXPECT_EQ(ROOT::Round(-190., 0.95, 0), "-190#pm1");
EXPECT_EQ(ROOT::Round(200., 0.95, 0), "200#pm1");
EXPECT_EQ(ROOT::Round(-210., 2.4), "-210#pm2");
EXPECT_EQ(ROOT::Round(220., 9.4), "220#pm9");
EXPECT_EQ(ROOT::Round(-0.001, 9.5), "-0#pm10");
EXPECT_EQ(ROOT::Round(230., 11.4), "230#pm11");
EXPECT_EQ(ROOT::Round(24., 19.4), "24#pm19");
EXPECT_EQ(ROOT::Round(-25., 19.5), "-30#pm20");
EXPECT_EQ(ROOT::Round(-25., 21, 9), "-25#pm21");
EXPECT_EQ(ROOT::Round(280., 94), "280#pm90");
EXPECT_EQ(ROOT::Round(-190., 95), "-190#pm100");
EXPECT_EQ(ROOT::Round(1., 101.4), "0#pm100");
EXPECT_EQ(ROOT::Round(-1., 109.4), "-0#pm110");
EXPECT_EQ(ROOT::Round(300., 119.4), "300#pm120");
EXPECT_EQ(ROOT::Round(-31., 119.5), "-30#pm120");
EXPECT_EQ(ROOT::Round(320., 194), "320#pm190");
EXPECT_EQ(ROOT::Round(-3030., 195), "-3000#pm200");
EXPECT_EQ(ROOT::Round(1400., 201), "1400#pm200");
EXPECT_EQ(ROOT::Round(-1200., 2000), "(-1#pm2)*1e3");
EXPECT_EQ(ROOT::Round(101., 2000, 2), "(0.1#pm2.0)*1e3");
EXPECT_EQ(ROOT::Round(-5056., 194, 9), "-5060#pm190");
EXPECT_EQ(ROOT::Round(-30000., 2000000000., 2), "(-0.0#pm2.0)*1e9");
EXPECT_EQ(ROOT::Round(-30000., 1000000000., 99), "(-0.0#pm1.0)*1e9");
EXPECT_EQ(ROOT::Round(-30000., 1000000000., 0), "(-0#pm1)*1e9");
EXPECT_EQ(ROOT::Round(110., 0.24, 1, "+-"), "110.0+-0.2");
}
Loading