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

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
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
57 changes: 57 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,57 @@ 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 nexp10 = std::floor(std::log10(error));
const auto scale = std::pow(10., nexp10);
const auto first_digit = static_cast<unsigned int>(error / scale);
assert (first_digit > 0 && first_digit < 10);
if (first_digit <= cutoff) {
const double rerror = error * std::pow(10., -1. * nexp10);
if (static_cast<unsigned int>(std::round(rerror * 10) / 10) <= cutoff)
nexp10--;
} else if (cutoff == 0 && first_digit == 9) {
const double rerror = std::round(error * std::pow(10., -1. * nexp10));
const int rnexp10 = std::floor(std::log10(rerror));
const auto rscale = std::pow(10., rnexp10);
const auto rfirst_digit = static_cast<int>(rerror / rscale);
if (rfirst_digit == 1)
nexp10++;
}

std::stringstream sv, se;
sv.setf(std::ios::fixed);
se.setf(std::ios::fixed);
const int maxExpo = error <= 1e-3 ? static_cast<int>(std::floor(std::log10(error) / 3)) * 3 : static_cast<int>(std::log10(error) / 3) * 3;
if(nexp10 - maxExpo < 0) {
sv.precision(-nexp10 + maxExpo);
se.precision(-nexp10 + maxExpo);
}
else {
sv.precision(0);
se.precision(0);
}
sv << std::round(value * std::pow(10., -nexp10))/std::pow(10., -nexp10 + maxExpo);
se << std::round(error * std::pow(10., -nexp10))/std::pow(10., -nexp10 + maxExpo);

return (maxExpo != 0 ? "(" : "") + sv.str() + std::string(delim) + se.str() + (maxExpo != 0 ? ")*1e" + std::to_string(maxExpo) : "") ;
}

} // 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