How do you std::round doubles, but round towards zero in halfway cases?

160 views Asked by At

I am in a situation where I need to round 0.5 and -0.5 down to 0. So I checked various documentations -

The generic C++ methods seems always round 0.5 away from 0.

https://en.cppreference.com/w/cpp/numeric/math/round

While if I go the lower level way by setting rounding style, it seems a very global approach and could potentially have threading and CPU state implication which is quite annoying.

https://en.cppreference.com/w/cpp/types/numeric_limits/float_round_style

Is there a light-weight, standard library and simple way to achieve rounding down to 0?

4

There are 4 answers

2
HolyBlackCat On BEST ANSWER
#include <cmath>

template <typename T>
T absmax(T a, T b)
{
    return std::abs(a) > std::abs(b) ? a : b;
}

template <typename T>
T round_ties_towards_zero(T x)
{
    return absmax(std::round(std::nextafter(x, T(0))), std::trunc(x));
}

For sufficiently small numbers you don't need absmax(..., trunc(x)). Without it, I start getting wrong answers for doubles at around 1e16 and larger.

2
Jan Schultke On

You can decompose a number into the integral and fractional part with std::modf. You can then build your own rounding mode on top of that:

template <std::floating_point T>
T round_to_nearest_ties_to_zero(T x)
{
    T integral;
    T fractional = std::modf(x, &integral);
    if (std::abs(fractional) <= T{0.5}) {
        return integral;
    }
    return integral + std::copysign(T{1}, x);
}
5
njuffa On

Basically, one wants to apply round(), i.e. round-to-nearest-ties-away, unless a tie case is detected, in which case one wants to apply trunc(), i.e. round-towards-zero. The simplest way to detect tie cases is to compute the absolute difference between x and round(x) and check whether it equals one half.

This algorithm is used in the C++11 code below, which I tested by using a three-way comparison with the two implementations proposed in the existing answers. No mismatches were found.

#include <cmath>

template <typename T>
T round_to_nearest_ties_toward_zero (T x)
{
    T rx = std::round (x);
    T tx = std::trunc (x);
    return (std::abs (x - rx) == T(0.5)) ? tx : rx;
}

On many platforms std::round() maps neither directly to a hardware instruction nor an inlined instruction sequence, but instead is a library call. If performance is a major concern, one could easily emulate std::round() as the addition of a rounding offset followed by std:trunc(). For a templated implementation the rounding offset would ideally computed as constexpr T rndofs = std::nextafter (T(0.5), T(0)); but I find that compilers do not accept that. I have tried to work around this in semi-portable fashion in the code below.

#include <cmath>
#include <limits>

template <typename T>
T round_to_nearest_ties_toward_zero (T x)
{
    constexpr int dm1 = std::numeric_limits<T>::digits - 1;
    constexpr T rndofs = T((1ULL << dm1) - 1) / T((1ULL << dm1)) / 2;
    T rx = std::trunc (x + std::copysign (rndofs, x));
    T tx = std::trunc (x);
    return (std::abs (x - rx) == T(0.5)) ? tx : rx;
#endif
}
0
chux - Reinstate Monica On

Variation on @Jan Schultke and @njuffy approach:

#include <cmath>

template <typename T>
T round_to_nearest_ties_toward_zero (T x) {
    T tx = std::trunc(x);
    return std::abs(x - tx) <= T(0.5) ? tx : tx + std::copysign(T(1.0), x);
    // or 
    return std::abs(x - tx) <= T(0.5) ? tx : std::round(x);
}