"human round"- Round to nearest, ties away from zero

Xianying Tan

2018/04/25

Have you ever noticed that both base::round(2.5) and base::round(1.5) return 2 in R? It’s strange, isn’t it? At least I learned only one rounding rule in school that is “四舍五入” in Chinese. It means we should round up if the decimal is five and down if four.

By reading the documentation of base::round() we know that there’s a standard called “IEEE 754” and the rounding rule that base::round() uses is called “go to the even digit”.

Note that for rounding off a 5, the IEC 60559 standard (see also ‘IEEE 754’) is expected to be used, ‘go to the even digit’.

Googling “IEEE 754” leads us to the Wikipedia page where lays five round rules. “To nearest, ties away from zero” is exactly the one that we’re familiar with. Due to unknow reasons, base::round() opts the first rule “to nearest, ties to even”, unfortunately.

Mode / Example Value 11.5 12.5 −11.5 −12.5
to nearest, ties to even 12 12 −12.0 −12.0
to nearest, ties away from zero 12 13 −12.0 −13.0
toward 0 11 12 −11.0 −12.0
toward +∞ 12 13 −11.0 −12.0
toward −∞ 11 12 −12.0 −13.0

Anyway, the only important question left is how to implement the human_round() in R? Despite the users won’t notice the “strange” rounding rule most of the time, I can imagine it could be very difficult to explain when asked. So we’d better have a solution. I’ll share it here.

The R version (recommend)

The rational here is that computers can only represent a fractional number in finite precision, meaning it’s safe to say two numbers are equal if the difference is smaller than a certain const. Usually, we choose .Machine$double.eps^0.5 as that const (Why this number? I steal it from dplyr::near() :P).

human_round_r <- function(x, digits = 0) {
  eps <- .Machine$double.eps^0.5
  flag_pos <- !is.na(x) & (x > 0)
  x[flag_pos] <- x[flag_pos] + eps
  flag_neg <- !is.na(x) & (x < 0)
  x[flag_neg] <- x[flag_neg] - eps
  round(x, digits = digits)
}
human_round_r(c(2.5, 1.5, -1.5, -2.5, 1.5 - .Machine$double.eps^0.5))
## [1]  3  2 -2 -3  2

Yes, it may not work as expected for the corner case like 1.5 - .Machine$double.eps^0.5 but the chance to get a number just equals to that in the real world closes to zero. Moreover, I can argue that 1.5 - .Machine$double.eps^0.5 and 1.5 is basically the same because of the precision mentioned above. However, if you insist a “perfect” one, please use the Rcpp version below.

The Rcpp version

It takes advantage that the std::round() in C++ uses the rounding rule we familiar with.

Rcpp::cppFunction("
NumericVector human_round_cpp(const NumericVector x, const int digits = 0)
{
  const double multiplier = std::pow(10, digits);
  NumericVector y(x.size());
  std::transform(x.begin(), x.end(), y.begin(),
                 [multiplier](const double x) {
                   if (!R_FINITE(x)) return x;
                   return std::round(x * multiplier) / multiplier;
                 });
  return y;
}")
human_round_cpp(c(2.5, 1.5, -1.5, -2.5, 1.5 - .Machine$double.eps^0.5))
## [1]  3  2 -2 -3  1