27

Is there is a simple, pythonic way of rounding to the nearest whole number without using floating point? I'd like to do the following but with integer arithmetic:

skip = int(round(1.0 * total / surplus))

==============

@John: Floating point is not reproducible across platforms. If you want your code to pass tests across different platforms then you need to avoid floating point (or add some hacky espilon stuff to your tests and hope it works). The above may be simple enough that it would be the same on most/all platforms, but I'd rather not make that determination as it is easier to avoid floating point altogether. How is that "not in the spirit of Python"?

new name
  • 15,861
  • 19
  • 68
  • 114
  • 8
    @John: Well, longs in Python can store arbitrarily-large values, where floats are fixed-precision, so there's a cost in range, complexity and possible bugs introducing floating-point into an integer operation. I do wish people would stop sprinkling every question with the silly buzzword "Pythonic", though. – Glenn Maynard Oct 16 '10 at 19:50
  • 2
    @GlennMaynard True! It's not very Pythical. – Rob Grant Apr 19 '15 at 21:40

7 Answers7

43

You can do this quite simply:

(n + d // 2) // d, where n is the dividend and d is the divisor.

Alternatives like (((n << 1) // d) + 1) >> 1 or the equivalent (((n * 2) // d) + 1) // 2 may be SLOWER in recent CPythons, where an int is implemented like the old long.

The simple method does 3 variable accesses, 1 constant load, and 3 integer operations. The complicated methods do 2 variable accesses, 3 constant loads, and 4 integer operations. Integer operations are likely to take time which depends on the sizes of the numbers involved. Variable accesses of function locals don't involve "lookups".

If you are really desparate for speed, do benchmarks. Otherwise, KISS.

John Machin
  • 81,303
  • 11
  • 141
  • 189
  • 2
    +1 This method is both more readable than the bit-shifting approach, and also faster (in timeit testing) on Py 2.7. – snapshoe Oct 17 '10 at 00:23
  • 1
    This will only implement the strategy "Round to Positive Infinity" e.g. def divRound(n, d): return (n + d // 2) // d then divRound(3, 6) is 1 and divRound(-3, 6) is 0. It will not implement the best rounding mode of IEEE, industry standard "Round to Nearest Even"...sadly... – Gregory Morse Sep 27 '22 at 16:53
  • And to add on what Gregory noted, with this rounding strategy it is semantically different from python's `round` function used by OP in the question, which also rounds 0.5 fractional parts to nearest even integer. – Daniel K Dec 07 '22 at 13:09
7

TL;DR:

q, r = divmod(total, surplus)
skip = q if q % 2 == 0 and 2*r == surplus else q + (2*r // surplus) # rounds to nearest integer; half to nearest even

This solution:

  • rounds to the nearest integer as demanded by OP
  • works for both positive and negative integers
  • rounds 0.5 fractional parts to nearest even integer (rnd(0.5)=0, rnd(1.5)=2) which matches the behavior of python's round function used by OP
  • sticks to integer arithmetic as demanded by OP (see documentation of divmod)

Full story

Inspired by zhmyh's answer answer, which is

q, r = divmod(total, surplus)
skip = q + int(bool(r)) # rounds to next greater integer (always ceiling)

, I came up with the following solution (UPDATE: that only works for non-negative total and surplus, as pointed out in the comments):

q, r = divmod(total, surplus)
skip = q + int(2 * r >= surplus) # rounds to nearest integer (floor or ceiling) if total and surplus are non-negative

Since the OP asked for rounding to the nearest whole number, zhmhs's solution is in fact slightly incorrect, because it always rounds to the next greater whole number, while my solution works as demanded works for non-negative total and surplus only. A correct solution that also works for negative numbers is:

q, r = divmod(total, surplus)
skip = q + (2 * r // surplus) # rounds to nearest integer (positive: half away from zero, negative: half toward zero)

Please note though that if 2 * r == surplus, it will basically perform ceiling for both positive and negative results, e.g. ceil(-1.5) == -1 whereas ceil(1.5) == 2. This is still correct behavior in terms of rounding to the nearest integer (because of equal distance to next lower and next greater integer) but it is asymmetrical in reference to zero. To also fix this, that is, round half away from zero, we can add a boolean condition:

q, r = divmod(total, surplus)
skip = q if q < 0 and 2*r == surplus else q + (2*r // surplus) # rounds to nearest integer; half away from zero

And even better, to round half to nearest even like python's round function used by the OP:

q, r = divmod(total, surplus)
skip = q if q % 2 == 0 and 2*r == surplus else q + (2*r // surplus) # rounds to nearest integer; half to nearest even

In case you wonder how divmod is defined: According to its documentation

For integers, the result is the same as (a // b, a % b).

We therefore stick with integer arithmetic, as demanded by the OP.

Daniel K
  • 2,977
  • 2
  • 24
  • 23
  • 1
    This answer is still incorrect. A "whole number" is also known as an integer and includes negative values. Therefore a correct implementation would use skip = q + (q >= 0) * (2 * r // surplus) – Gregory Morse Sep 27 '22 at 17:58
  • @GregoryMorse You are right, thanks for pointing it out! I edited the answer and included your fix, thank you! – Daniel K Dec 05 '22 at 12:31
  • @GregoryMorse I thought about it again and realized that your proposed fix is close to being correct but always performs flooring for negative results, so it is also not fully correct, e.g. `total = -9`, `surplus = 7` yields `skip = -2` instead of `skip = -1`. I believe I fixed it now though in my answer by just removing the boolean condition. – Daniel K Dec 05 '22 at 15:34
  • There are many details to consider. Round to nearest away from zero, round to nearest towards zero, round to nearest towards negative infinity (floor), round to nearest towards positive infinity (ceiling), etc. Actually the most accepted equitable rounding for scientific purposes is round to nearest even e.g. the IEEE-754 floating point default. My solution I believe was round away from zero. – Gregory Morse Dec 06 '22 at 17:25
  • As OP wants to round to the nearest integer, I think you are referring to the rounding strategies for the 0.5 fractional parts, that is, `x.5` or `-x.5`. True, but your solution for rounding half away from zero still has an issue, in the above mentioned example the result should be `skip = -1` for any half rounding strategy, because (`-9/7 = -1.3`). Anyhow, I included another fix to perform half away from zero rounding (even though half to nearest even rounding would be even better, as you also stated, because it would match python's `round` function behavior). – Daniel K Dec 07 '22 at 11:57
  • UPDATE: I changed the boolean condition to perform half to nearest even rounding as discussed. This should match OP's requirements best. – Daniel K Dec 07 '22 at 13:17
  • Yes true it is incorrect for a round to nearest strategies. However for rounding outside of the nearest strategies, there are also all these variants and it could be correct in that context. – Gregory Morse Dec 08 '22 at 14:15
  • I would argue that unnecessary extra divisions are not a good solution and logic-based solutions are better, especially for arbitrary sized integers. Comparison and logic is far more straight forward and elegant and with better performance. I have updated my answer below for every scheme nearest variants and absolute rounding cases both. Away from zero is q + (q >= 0) * (2 * r == d) + ((2 * r < d) if d < 0 else (2 * r > d)) while q>=0 substituted for q&1 or q%2==0 provides the nearest even case. – Gregory Morse Dec 09 '22 at 11:22
7
skip = (((total << 1) // surplus) + 1) >> 1

Shifting things left by one bit effectively multiplies by two, shifting things right by one bit divides by two rounding down. Adding one in the middle makes it so that "rounding down" is actually rounding up if the result would have been above a .5 decimal part.

It's basically the same as if you wrote...

skip = int((1.0*total/surplus) + 0.5)

except with everything multplied by 2, and then later divided by 2, which is something you can do with integer arithmetic (since bit shifts don't require floating point).

Amber
  • 507,862
  • 82
  • 626
  • 550
  • Right idea, but I think the quantity you need to add to `total` is commensurate to `surplus`. I would replace the "+ 1" by "+ surplus" in your current formula and that would probably be about right. – Pascal Cuoq Oct 16 '10 at 19:23
  • Actually I just need to move the 1 outside. :) It's equivalent to adding surplus inside, but requires fewer lookups. – Amber Oct 16 '10 at 19:24
  • Thanks! It is not immediately obvious to me that this works correctly in all the edge cases, and I'll check to be sure. – new name Oct 16 '10 at 19:31
  • 1
    I'd recommend multiplying multiplying by two to multiply by two, and dividing by two to divide by two, unless this is actually profiled, performance-sensitive code. – Glenn Maynard Oct 16 '10 at 19:59
  • This is again Round to Positive Infinity rounding mode, not the industry standard round to nearest even. – Gregory Morse Sep 27 '22 at 16:54
2

Yet another funny way:

q, r = divmod(total, surplus)
skip = q + int(bool(r))
rmnff
  • 386
  • 2
  • 4
  • Note that this solution rounds to to next greater whole number which is not necessarily the nearest whole number. See my answer for a fixed version _(which I posted at a time when I did not have enough reputation for commenting yet.)_. – Daniel K Jun 03 '14 at 16:38
0

Simply take care of the rounding rule before you ever divide. For the simplest round-half-up:

if total % surplus < surplus / 2:
    return total / surplus
else:
    return (total / surplus) + 1

Tweak a little bit if you need to do a proper round-to-even.

hobbs
  • 223,387
  • 19
  • 210
  • 288
  • 1
    The modulo and the division operator are quite expensive, this code runs 3 division operations (one modulo and 2 regular divisions), so this is not optimal if the code needs to be fast. – FrederikNS Nov 17 '11 at 00:59
  • Actually there is a conditional so it runs only a module and division. It should use divmod instead obviously. I posted a method to do Round To Nearest even as a separate answer. – Gregory Morse Sep 27 '22 at 23:32
-1

The question is the rounding strategy you are after.

Here are several that I came up with - note that integer floor and ceiling are cases, but in "round to nearest" there are many strategies. The IEEE 754 and the most professional and industrial and even elegant way of doing it is Round To Nearest Even. I've never seen a single example anywhere of doing this for integer arithmetic. You cannot go through float as the 53-bit mantissa may cause precision issues and it already applies round-to-nearest-even mode if desiring to do a different rounding strategy. The correct techniques should always stay in the integer domain

def roundNegInf(n, d): #floor
    return n // d
def roundPosInf(n, d): #ceil
    return (n + d + -1*(d >= 0)) // d
def roundTowardsZero(n, d):
    return (n + ((d + -1*(d >=0)) if (n < 0) ^ (d < 0) else 0)) // d
def roundAwayFromZero(n, d):
    return (n + (0 if (n < 0) ^ (d < 0) else (d + -1*(d >= 0)))) // d
def roundNearestPosInf(n, d):
    #return (((n << 1) // d) + 1) >> 1
    #return (((n * 2) // d) + 1) // 2
    q, r = divmod(n, d)
    return q + (2 * r <= d if d < 0 else 2 * r >= d)
def roundNearestNegInf(n, d):
    q, r = divmod(n, d)
    return q + (2 * r < d if d < 0 else 2 * r > d)
def roundNearestEven(n, d): #round only when for positive numbers guard, round, sticky are 11X or 1X1
    q, r = divmod(n, d)
    return q + (q & 1) * (2 * r == d) + ((2 * r < d) if d < 0 else (2 * r > d))
def roundNearestToZero(n, d):
    q, r = divmod(n, d)
    return q + (q < 0) * (2 * r == d) + ((2 * r < d) if d < 0 else (2 * r > d))
def roundNearestAwayZero(n, d):
    q, r = divmod(n, d)
    return q + (q >= 0) * (2 * r == d) + ((2 * r < d) if d < 0 else (2 * r > d))
def testRounding():
    pairs = ((1, 2), (-1, 2), (1, -2), (-1, -2), (3, 2), (-3, 2), (3, -2), (-3, -2),
             (1, 3), (-1, 3), (1, -3), (-1, -3), (2, 3), (-2, 3), (2, -3), (-2, -3))
    funcs = (roundNegInf, roundPosInf, roundTowardsZero, roundAwayFromZero,
             roundNearestPosInf, roundNearestNegInf,
             roundNearestToZero, roundNearestAwayZero, roundNearestEven)
    res = [[f(*p) for p in pairs] for f in funcs]
    expected = [[0, -1, -1, 0, 1, -2, -2, 1, 0, -1, -1, 0, 0, -1, -1, 0],
                   [1, 0, 0, 1, 2, -1, -1, 2, 1, 0, 0, 1, 1, 0, 0, 1],
                   [0, 0, 0, 0, 1, -1, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
                   [1, -1, -1, 1, 2, -2, -2, 2, 1, -1, -1, 1, 1, -1, -1, 1],
                   [1, 0, 0, 1, 2, -1, -1, 2, 0, 0, 0, 0, 1, -1, -1, 1],
                   [0, -1, -1, 0, 1, -2, -2, 1, 0, 0, 0, 0, 1, -1, -1, 1],
                   [0, 0, 0, 0, 1, -1, -1, 1, 0, 0, 0, 0, 1, -1, -1, 1],
                   [1, -1, -1, 1, 2, -2, -2, 2, 0, 0, 0, 0, 1, -1, -1, 1],
                   [0, 0, 0, 0, 2, -2, -2, 2, 0, 0, 0, 0, 1, -1, -1, 1]
                   ]
    assert(all([x == y for x, y in zip(res, expected)]))
Gregory Morse
  • 369
  • 2
  • 10
-2

This should work too:

def rint(n):
    return (int(n+.5) if n > 0 else int(n-.5))
rubik
  • 8,814
  • 9
  • 58
  • 88
  • This is not an answer to the question, because it involves floating point arithmetic in `n + .5`. @rubik I didn't downvote, that was someone else. ;-) – Arne L. Jul 30 '13 at 16:40
  • 1
    @ArneL.: Well it does not matter, if it's wrong it's right to downvote :) – rubik Jul 30 '13 at 16:42
  • This is "Round to nearest, ties away from zero" strategy. Still not the most elegant Round To Nearest Even. – Gregory Morse Sep 27 '22 at 16:55