Published by
Aug 18, 2008

Rounding Algorithms

Score: 4.4/5 (19 votes)
*****
There are a zillion different ways to round floating point values to integers. C and C++ provide a couple basic ones in <math.h> or <cmath>.

There are two general categories of rounding algorithm: those that are symmetric about zero and those that are biased in some way.

Biased Rounding A bias is a mathematical notion used in statistics to indicate that samples are not exactly representative of their true values; they are skewed in one way or another.

For example, the <cmath> floor() function is biased towards negative infinity, because it always chooses the lower integer number --that is, it always chooses the number closer to negative infinity.
floor( 7.5 ) --> 7
floor( -7.5 ) --> -8

Suppose your local big city and wanted to know how fast people are driving on a particular freeway. The first step is to gather the exact speeds that individual drivers are going, and the second would be to convert all the individual values into a single value that would represent the normal rate of speed. For simplicity here, we will just use the average value.

Let us say that the equipment which samples a motorist's speed is far more accurate than the computer's ability to store it. (This is actually not uncommon.) Again, for simplicity, we will say that the computer stores the speed as integers.

With a sampling of twelve motorists, we get the following speeds (in miles per hour)
49.087 57.901 28.500 46.738 51.270 53.096
44.795 47.218 46.347 45.989 47.582 50.563
A quick calculation shows that the average speed is 47.424 mph.

If the city were to simply use the floor() function to convert these to integers, it would get
49 57 28 46 51 53
44 47 46 45 47 50
which averages to 46.916 --> 46 mph (remember, integer arithmetic!)

Either way, the sampling is off by about a whole mile per hour. I don't think that the city would actually care about a single mile per hour, but this does illustrate the bias, or tendancy of the floor() function, to make the numbers closer to negative infinity, thuse skewing the data to an inaccurate number.

This was just a simple example that came off the top of my head, but in many sciences and statistical surveys, that difference can mean quite a lot. Suppose that the Apollo missed the moon by 1%? Suppose a pharmaceutical company put too much iron in a daily vitamin pill by 1%? Suppose a construction firm miscalculated the stresses a bridge can take by 1%? In all these scenarios the results would prove deadly. One percent is a lot.


Symmetric Rounding A special case of bias is centered about zero. Let us fix the floor() function to tend towards zero.
1
2
3
4
5
6
7
double floor0( double value )
  {
  if (value < 0.0)
    return ceil( value );
  else
    return floor( value );
  }

Now, the absolute value of the result will always be the same:
floor0( -7.7 ) --> -7 floor0( 7.7 ) --> 7
floor0( -7.5 ) --> -7 floor0( 7.5 ) --> 7
floor0( -7.3 ) --> -7 floor0( 7.3 ) --> 7
Enough about that.


Unbiased Rounding So, how do we handle these biases? By adding some rule that accounts for it.

Let us apply knowledge we all learned in gradeschool: In arithmetic rounding we round up if the next digit is 5 or more, and if it less than 5 we round down. We write ourselves a little function to do just that:
1
2
3
4
double round( double value )
  {
  return floor( value + 0.5 );
  }

The problem is that this is still biased. We have actually reversed the bias of floor() from negative infinity to positive infinity, because we always choose to round up when exactly halfway between two values:
round( 10.3 ) --> 10
round( 10.5 ) --> 11
round( 10.7 ) --> 11
You can actually see the bias in the above table: the result tends towards 11 and away from 10.

This brings us to the trick: which way do we round when exactly halfway between two values?

One very popular method is variously called "bankers rounding", "round to even", "convergent rounding", and even "unbiased rounding", to name a few. It works by skewing the bias itself.

Given a number exactly halfway between two values, round to the even value (zero is considered even here).
round( 1.7 ) --> 2 round( 2.7 ) --> 3
round( 1.5 ) --> 2 round( 2.5 ) --> 2
round( 1.3 ) --> 1 round( 2.3 ) --> 2
For random data this is very convenient. Bankers like it because money deposited and withdrawn is random. (There are trends, mind you, but you cannot predict exactly how much will be deposited and withdrawn.) The important point is that the bankers rounding is still biased if the data is biased. It is only unbiased with random data.

One solution is called "alternate rounding". It works by simply choosing to bias up or down every other time.
round( 1.5 ) --> 2
round( 1.5 ) --> 1
round( 1.5 ) --> 2
round( 1.5 ) --> 1
etc
This is not always useful though.

The only way to eliminate all bias is to use a random bias... This, of course, is impossible to generate in your typical PC, but it still goes toward solving the problem quite nicely.

If the sample is exactly halfway between two integers, it chooses one or the other randomly.

Of course, the Achilles heel of this method is the random number generator you use. The default pseudorandom generator for C and C++ is not that great. The Mersenne Twister is by far the most popular high-quality pseudorandom number generator, but it is non-trivial to implement so I will not include it below.

Anyway, what follows is a convenient, simple library you are free to use. I'll even permit you to cannabalize it at will (since the algorithms are so obvious...)

I may update it sometime in the future if I can figure out a way around the default epsilon issue. Feel free to make suggestions for improvements.

:-)
rounding-algorithms.hpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
// rounding-algorithms.hpp
//
// General Rounding Algorithms
// Copyright (c) 2008 Michael Thomas Greer
// Boost Software License - Version 1.0 - August 17th, 2003
//
// Permission is hereby granted, free of charge, to any person or organization
// obtaining a copy of the software and accompanying documentation covered by
// this license (the "Software") to use, reproduce, display, distribute,
// execute, and transmit the Software, and to prepare derivative works of the
// Software, and to permit third-parties to whom the Software is furnished to
// do so, all subject to the following:
//
// The copyright notices in the Software and this entire statement, including
// the above license grant, this restriction and the following disclaimer,
// must be included in all copies of the Software, in whole or in part, and
// all derivative works of the Software, unless such copies or derivative
// works are solely in the form of machine-executable object code generated by
// a source language processor.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//
//----------------------------------------------------------------------------
// Reference
// <<a href="http://www.pldesignline.com/howto/showArticle.jhtml;?articleID=175801189>">http://www.pldesignline.com/howto/showArticle.jhtml;?articleID=175801189></a>
//
//----------------------------------------------------------------------------
// In this library, symmetric functions are indicated by a zero at the end
// of the function name.
//
// If you want a different default epsilon make sure to change
//
//   #define ROUNDING_EPSILON 0.001
//
// to whatever you want it to be. (I wanted to make it so that you could 
// define a different default epsilon each time you #included the file, but
// I haven't figured out how to get around the template restrictions yet.)
//

#ifndef ROUNDING_ALGORITHMS_HPP
#define ROUNDING_ALGORITHMS_HPP

#ifndef ROUNDING_EPSILON
#define ROUNDING_EPSILON 0.0000001
#endif

#include <cmath>
#include <cstdlib>
#include <ciso646>

namespace rounding
  {

  //--------------------------------------------------------------------------
  // round down
  // Bias: -Infinity
  using std::floor;

  //--------------------------------------------------------------------------
  // round up
  // Bias: +Infinity
  using std::ceil;

  //--------------------------------------------------------------------------
  // symmetric round down
  // Bias: towards zero
  template <typename FloatType>
  FloatType floor0( const FloatType& value )
    {
    FloatType result = std::floor( std::fabs( value ) );
    return (value < 0.0) ? -result : result;
    }

  //--------------------------------------------------------------------------
  // A common alias for floor0()
  // (notwithstanding hardware quirks)
  template <typename FloatType>
  inline
  FloatType trunc( const FloatType& value )
    {
    return floor0( value );
    }

  //--------------------------------------------------------------------------
  // symmetric round up
  // Bias: away from zero
  template <typename FloatType>
  FloatType ceil0( const FloatType& value )
    {
    FloatType result = std::ceil( std::fabs( value ) );
    return (value < 0.0) ? -result : result;
    }

  //--------------------------------------------------------------------------
  // Common rounding: round half up
  // Bias: +Infinity
  template <typename FloatType>
  FloatType roundhalfup( const FloatType& value )
    {
    return std::floor( value +0.5 );
    }

  //--------------------------------------------------------------------------
  // Round half down
  // Bias: -Infinity
  template <typename FloatType>
  FloatType roundhalfdown( const FloatType& value )
    {
    return std::ceil( value -0.5 );
    }

  //--------------------------------------------------------------------------
  // symmetric round half down
  // Bias: towards zero
  template <typename FloatType>
  FloatType roundhalfdown0( const FloatType& value )
    {
    FloatType result = roundhalfdown( std::fabs( value ) );
    return (value < 0.0) ? -result : result;
    }

  //--------------------------------------------------------------------------
  // symmetric round half up
  // Bias: away from zero
  template <typename FloatType>
  FloatType roundhalfup0( const FloatType& value )
    {
    FloatType result = roundhalfup( std::fabs( value ) );
    return (value < 0.0) ? -result : result;
    }

  //--------------------------------------------------------------------------
  // round half even (banker's rounding)
  // Bias: none
  template <typename FloatType>
  FloatType roundhalfeven(
    const FloatType& value,
    const FloatType& epsilon = ROUNDING_EPSILON
    ) {
    if (value < 0.0) return -roundhalfeven <FloatType> ( -value, epsilon );

    FloatType ipart;
    std::modf( value, &ipart );

    // If 'value' is exctly halfway between two integers
    if ((value -(ipart +0.5)) < epsilon)
      {
      // If 'ipart' is even then return 'ipart'
      if (std::fmod( ipart, 2.0 ) < epsilon)
        return ipart;

      // Else return the nearest even integer
      return ceil0( ipart +0.5 );
      }

    // Otherwise use the usual round to closest
    // (Either symmetric half-up or half-down will do0
    return roundhalfup0( value );
    }

  //--------------------------------------------------------------------------
  // round alternate
  // Bias: none for sequential calls
  bool _is_up = false;
  template <typename FloatType>
  FloatType roundalternate( const FloatType& value, int& is_up = _is_up )
    {
    if ((is_up != is_up))
      return roundhalfup( value );
    return roundhalfdown( value );
    }

  //--------------------------------------------------------------------------
  // symmetric round alternate
  // Bias: none for sequential calls
  template <typename FloatType>
  FloatType roundalternate0( const FloatType& value, int& is_up = _is_up )
    {
    if ((is_up != is_up))
      return roundhalfup0( value );
    return roundhalfdown0( value );
    }

  //--------------------------------------------------------------------------
  // round random
  // Bias: generator's bias
  template <typename FloatType, typename RandValue, typename RandomGenerator>
  FloatType roundrandom(
    const FloatType& value,
    const RandValue& mid,
    RandomGenerator& g
    ) {
    if (g() < mid)
      return roundhalfup0( value );
    return roundhalfdown0( value );
    }

  //--------------------------------------------------------------------------
  // default round random
  // Bias: rand()
  template <typename FloatType>
  FloatType roundrandom( const FloatType& value )
    {
    return roundrandom <FloatType, int, int(*)()> ( value, RAND_MAX /2, &rand );
    }
  }

#endif 


If you find a bug let me know!
The round half down did exactly what it should have. Given a number exactly halfway between two integers (12.5) it rounds down to 12.

As an unrelated point of interest, if you are passing typed arguments to a template function you don't actually have to give template arguments:
1
2
3
4
5
double num = 12.5;
cout << roundhalfdown( num ) << endl;

num = 12.500001;
cout << roundhalfdown( num ) << endl;

One thing to remember about floating point numbers is that they are not exact. In fact, a lot more can go wrong with them than people realize. So, depending on your hardware and your clib's temperament that second one may or may not be considered the same as the first. IEEE single precision should handle it fine though.