3

I want a overflow-safe function that round a double like std::round in addition it can handle the number of significant decimal digts.

f.e.

round(-17.747, 2) -> -17.75
round(-9.97729, 2) -> -9.98
round(-5.62448, 2) -> -5.62
round(std::numeric_limits<double>::max(), 10) ...

My first attempt was

double round(double value, int precision)
{
    double factor=pow(10.0, precision);
    return floor(value*factor+0.5)/factor;
}

but this can easily overflow.

Assuming IEEE, it is possible to decrease the possibility of overflows, like this.

double round(double value, int precision)
{
    // assuming IEEE 754 with 64 bit representation
    // the number of significant digits varies between 15 and 17

    precision=std::min(17, precision);
    double factor=pow(10.0, precision);
    return floor(value*factor+0.5)/factor;
}

But this still can overflow.

Even this performance disaster does not work.

double round(double value, int precision)
{
    std::stringstream ss;
    ss << std::setprecision(precision) << value;
    std::string::size_type sz;
    return std::stod(ss.str(), &sz);
}
round(std::numeric_limits<double>::max(), 2.0) // throws std::out_of_range

Note:

  • I'm aware of setprecision, but i need rounding not only for displaying purpose. So that is not a solution.
  • Unlike this post here How to round a number to n decimal places in Java , my question is especially on overflow safety and in C++ (the anwser in the topic above are Java-specific or do not handle overflows)
user1235183
  • 2,770
  • 21
  • 56
  • You can't. Floating-point doesn't have decimal places, so you can't round to them. It has binary places, which are incommensurable with decimal places. If you want decimal digits you have to use a decimal radix. – user207421 Mar 03 '20 at 09:23
  • 1
    @user207421: I don't understand how the Java answers are directly beneficial to the OP. – jxh Mar 03 '20 at 09:43
  • @jxh can you convert you comment into an anwser by showing more details? You may also reopen this question single-handedly as C++ gold badge owner. – user1235183 Mar 03 '20 at 09:53
  • I wonder what you want to happen in case of overflow? In your example you get `out_of_range` if you input `1.79769e+308`. This is the biggest number you can represent as a float. Rounding this to two decimals is `1.8e+308`. This cannot be represented as an IEEE floating point number since it is just too big. Together with user207421's answer, I wonder what is the purpose of rounding to *decimal* places for anything else but output? You will never get exact decimal places for all numbers. The only exact thing you can get are binary places. Would `libgmp` help with overflow issues? – Daniel Junglas Mar 03 '20 at 10:44
  • 1
    @DanielJunglas: If I understood the requirements, the rounding is the number of fractional decimal places. Since `1.79769e+308` does not have a fractional part, the value doesn't change. – jxh Mar 03 '20 at 10:46
  • @jxh, ok, I see, thanks. But then the example code using `sstream` can be easily changed: Test whether the number is integer. If so then return it, otherwise do as in the provided code. Numbers that can cause an overflow never have a fractional part. Or am I missing something? – Daniel Junglas Mar 03 '20 at 10:52
  • @DanielJunglas: I am not certain what the underlying requirements are. BCD has its uses, but there are libraries dedicated to providing it. – jxh Mar 03 '20 at 10:59
  • @jxh *My* answer in the duplicate is useful to the OP as a demonstration of why it can't be done. Most of the rest of them are wrong. – user207421 Mar 04 '20 at 03:11
  • @user207421 While in the general case you cannot round a `double` to an exact number of fractional decimal places, you can round it to an exact number of fractional binary places. It is not obvious to most askers of this kind of question that this is what they should do if they want to carry around fewer bits to represent the number. – jxh Mar 04 '20 at 07:34

1 Answers1

2

I haven't heavily tested this code:

/* expects x in (-1, 1) */
double round_precision2(double x, int precision2) {
    double iptr, factor = std::exp2(precision2);
    double y = (x < 0) ? -x : x;
    std::modf(y * factor + .5, &iptr);
    return iptr/factor * ((x < 0) ? -1 : 1);
}

double round_precision(double x, int precision) {
    int bits = precision * M_LN10 / M_LN2;
            /* std::log2(std::pow(10., precision)); */
    double iptr, frac = std::modf(x, &iptr);
    return iptr + round_precision2(frac, bits);
}

The idea is to avoid overflow by only operating on the fractional part of the number.

We compute the number of binary bits to achieve the desired precision. You should be able to put a bound on them with the limits you describe in your question. Next, we extract the fractional and integer parts of the number. Then we add the integer part back to the rounded fractional part.

To compute the rounded fractional part, we compute the binary factor. Then we extract the integer part of the rounded number resulting from multiplying fractional part by the factor. Then we return the fraction by dividing the integral part by the factor.

jxh
  • 64,506
  • 7
  • 96
  • 165