'Python round a float to nearest 0.05 or to multiple of another float

I want to emulate this function. I want to round a floating point number down to the nearest multiple of 0.05 (or generally to the nearest multiple of anything).

I want this:

>>> round_nearest(1.29, 0.05)
1.25

>>> round_nearest(1.30, 0.05)
1.30

I can do this:

import math

def round_nearest(n, r):
    return n - math.fmod(n, r)

>>> round_nearest(1.27, 0.05)
1.25  # Correct!

>>> round_nearest(1.30, 0.05)
1.25  # Incorrect! Correct would be 1.30.

The incorrect answer above is presumably due to floating point rounding. I could put some special case check to see if the n is "close enough" to a multiple of r and not do the subtraction, and that would probably work, but is there a better way? Or is this strategy the best option?



Solution 1:[1]

You can round down to the nearest multiple of a like this:

def round_down(x, a):
    return math.floor(x / a) * a

You can round to the nearest multiple of a like this:

def round_nearest(x, a):
    return round(x / a) * a

Solution 2:[2]

As Paul wrote:

You can round to the nearest multiple of a like this:

def round_nearest(x, a):
    return round(x / a) * a

Works nearly perfectly, but round_nearest(1.39, 0.05) gives 1.4000000000000001. To avoid it I'll recommend to do:

import math

def round_nearest2(x, a):
    return round(round(x / a) * a, -int(math.floor(math.log10(a))))

Which rounds to precision a, and then to number of significant digits, that has your precision a

EDIT

As @Asclepius shown this code has limitation to the first digit in precision (meaning that e.g. if you put 4.3 then rounding is done to closest integer, if you put 0.25 then number is rounded to first decimal digit after all. This can be easily fix by finding how many digits actually precision contains, and rounding to this number after all:

def round_nearest(x, a):
    max_frac_digits = 100
    for i in range(max_frac_digits):
        if round(a, -int(math.floor(math.log10(a))) + i) == a:
            frac_digits = -int(math.floor(math.log10(a))) + i
            break
    return round(round(x / a) * a, frac_digits)

frac_digits is rounded log10 of your precision (nearest number), so it basically shows how many fractional digits should be taken into account (or in case of bigger number - integer digits). So if your precision is 0.25 then frac_digits will be equal to 2, because of 2 fractional digits. If your precision is 40 then frac_digits will be equal to -1, because you need to 'go back' one digit from decimal separator.

Solution 3:[3]

The previous answer by Paul fails the test round_down(4.6, 0.2) == 4.6.

This answer has two types of solutions, inexact and exact. They pass all previous tests and more, also with negative numbers. Each approach provides solutions for round_nearest, round_down, and round_up.

As a disclaimer, these solutions require a lot more testing. Where math.isclose is used, its default tolerances apply.

Can you find a failing example?

To devise additional exact solutions, consider this reference.

Using round (inexact)

import math

def round_nearest(num: float, to: float) -> float:
    return round(num / to) * to  # Credited to Paul H.

def round_down(num: float, to: float) -> float:
    nearest = round_nearest(num, to)
    if math.isclose(num, nearest): return num
    return nearest if nearest < num else nearest - to

def round_up(num: float, to: float) -> float:
    nearest = round_nearest(num, to)
    if math.isclose(num, nearest): return num
    return nearest if nearest > num else nearest + to

# Tests:
rn, rd, ru = round_nearest, round_down, round_up

> rn(1.27, 0.05), rn(1.29, 0.05), rn(1.30, 0.05), rn(1.39, 0.05)
(1.25, 1.3, 1.3, 1.4000000000000001)
> rn(-1.27, 0.05), rn(-1.29, 0.05), rn(-1.30, 0.05), rn(-1.39, 0.05)
(-1.25, -1.3, -1.3, -1.4000000000000001)
> rn(4.4, 0.2), rn(4.5, 0.2), rn(4.6, 0.2)
(4.4, 4.4, 4.6000000000000005)
> rn(-4.4, 0.2), rn(-4.5, 0.2), rn(-4.6, 0.2)
(-4.4, -4.4, -4.6000000000000005)
> rn(82, 4.3)
81.7

> rd(1.27, 0.05), rd(1.29, 0.05), rd(1.30, 0.05)
(1.25, 1.25, 1.3)
> rd(-1.27, 0.05), rd(-1.29, 0.05), rd(-1.30, 0.05)
(-1.3, -1.3, -1.3)
> rd(4.4, 0.2), rd(4.5, 0.2), rd(4.6, 0.2)
(4.4, 4.4, 4.6)
> rd(-4.4, 0.2), rd(-4.5, 0.2), rd(-4.6, 0.2)
(-4.4, -4.6000000000000005, -4.6)

> ru(1.27, 0.05), ru(1.29, 0.05), ru(1.30, 0.05)
(1.3, 1.3, 1.3)
> ru(-1.27, 0.05), ru(-1.29, 0.05), ru(-1.30, 0.05)
(-1.25, -1.25, -1.3)
> ru(4.4, 0.2), ru(4.5, 0.2), ru(4.6, 0.2)
(4.4, 4.6000000000000005, 4.6)
> ru(-4.4, 0.2), ru(-4.5, 0.2), ru(-4.6, 0.2)
(-4.4, -4.4, -4.6)

Using math.fmod (inexact)

import math

def round_down(num: float, to: float) -> float:
    if num < 0: return -round_up(-num, to)
    mod = math.fmod(num, to)
    return num if math.isclose(mod, to) else num - mod

def round_up(num: float, to: float) -> float:
    if num < 0: return -round_down(-num, to)
    down = round_down(num, to)
    return num if num == down else down + to

def round_nearest(num: float, to: float) -> float:
    down, up = round_down(num, to), round_up(num, to)
    return down if ((num - down) < (up - num)) else up

# Tests:
rd, ru, rn = round_down, round_up, round_nearest

> rd(1.27, 0.05), rd(1.29, 0.05), rd(1.30, 0.05)
(1.25, 1.25, 1.3)
> rd(-1.27, 0.05), rd(-1.29, 0.05), rd(-1.30, 0.05)
(-1.3, -1.3, -1.3)
> rd(4.4, 0.2), rd(4.5, 0.2), rd(4.6, 0.2)
(4.4, 4.4, 4.6)
> rd(-4.4, 0.2), rd(-4.5, 0.2), rd(-4.6, 0.2)
(-4.4, -4.6000000000000005, -4.6)

> ru(1.27, 0.05), ru(1.29, 0.05), ru(1.30, 0.05)
(1.3, 1.3, 1.3)
> ru(-1.27, 0.05), ru(-1.29, 0.05), ru(-1.30, 0.05)
(-1.25, -1.25, -1.3)
> ru(4.4, 0.2), ru(4.5, 0.2), ru(4.6, 0.2)
(4.4, 4.6000000000000005, 4.6)
> ru(-4.4, 0.2), ru(-4.5, 0.2), ru(-4.6, 0.2)
(-4.4, -4.4, -4.6)

> rn(1.27, 0.05), rn(1.29, 0.05), rn(1.30, 0.05), rn(1.39, 0.05)
(1.25, 1.3, 1.3, 1.4000000000000001)
> rn(-1.27, 0.05), rn(-1.29, 0.05), rn(-1.30, 0.05), rn(-1.39, 0.05)
(-1.25, -1.3, -1.3, -1.4000000000000001)
> rn(4.4, 0.2), rn(4.5, 0.2), rn(4.6, 0.2)
(4.4, 4.4, 4.6)
> rn(-4.4, 0.2), rn(-4.5, 0.2), rn(-4.6, 0.2)
(-4.4, -4.4, -4.6)
> rn(82, 4.3)
81.7

Using math.remainder (inexact)

This section implements only round_nearest. For round_down and round_up, use the same exact logic as in the "Using round" section.

def round_nearest(num: float, to: float) -> float:
    return num - math.remainder(num, to)

# Tests:
rn = round_nearest

> rn(1.27, 0.05), rn(1.29, 0.05), rn(1.30, 0.05), rn(1.39, 0.05)
(1.25, 1.3, 1.3, 1.4000000000000001)
> rn(-1.27, 0.05), rn(-1.29, 0.05), rn(-1.30, 0.05), rn(-1.39, 0.05)
(-1.25, -1.3, -1.3, -1.4000000000000001)
> rn(4.4, 0.2), rn(4.5, 0.2), rn(4.6, 0.2)
(4.4, 4.4, 4.6000000000000005)
> rn(-4.4, 0.2), rn(-4.5, 0.2), rn(-4.6, 0.2)
(-4.4, -4.4, -4.6000000000000005)
> rn(82, 4.3)
81.7

Using decimal.Decimal (exact)

Note that this is an inefficient solution because it uses str.

from decimal import Decimal
import math

def round_nearest(num: float, to: float) -> float:
    num, to = Decimal(str(num)), Decimal(str(to))
    return float(round(num / to) * to)

def round_down(num: float, to: float) -> float:
    num, to = Decimal(str(num)), Decimal(str(to))
    return float(math.floor(num / to) * to)

def round_up(num: float, to: float) -> float:
    num, to = Decimal(str(num)), Decimal(str(to))
    return float(math.ceil(num / to) * to)

# Tests:
rn, rd, ru = round_nearest, round_down, round_up

> rn(1.27, 0.05), rn(1.29, 0.05), rn(1.30, 0.05), rn(1.39, 0.05)
(1.25, 1.3, 1.3, 1.4)
> rn(-1.27, 0.05), rn(-1.29, 0.05), rn(-1.30, 0.05), rn(-1.39, 0.05)
(-1.25, -1.3, -1.3, -1.4)
> rn(4.4, 0.2), rn(4.5, 0.2), rn(4.6, 0.2)
(4.4, 4.4, 4.6)
> rn(-4.4, 0.2), rn(-4.5, 0.2), rn(-4.6, 0.2)
(-4.4, -4.4, -4.6)
> rn(82, 4.3)
81.7

> rd(1.27, 0.05), rd(1.29, 0.05), rd(1.30, 0.05)
(1.25, 1.25, 1.3)
> rd(-1.27, 0.05), rd(-1.29, 0.05), rd(-1.30, 0.05)
(-1.3, -1.3, -1.3)
> rd(4.4, 0.2), rd(4.5, 0.2), rd(4.6, 0.2)
(4.4, 4.4, 4.6)
> rd(-4.4, 0.2), rd(-4.5, 0.2), rd(-4.6, 0.2)
(-4.4, -4.6, -4.6)

> ru(1.27, 0.05), ru(1.29, 0.05), ru(1.30, 0.05)
(1.3, 1.3, 1.3)
> ru(-1.27, 0.05), ru(-1.29, 0.05), ru(-1.30, 0.05)
(-1.25, -1.25, -1.3)
> ru(4.4, 0.2), ru(4.5, 0.2), ru(4.6, 0.2)
(4.4, 4.6, 4.6)
> ru(-4.4, 0.2), ru(-4.5, 0.2), ru(-4.6, 0.2)
(-4.4, -4.4, -4.6)

Using fractions.Fraction (exact)

Note that this is an inefficient solution because it uses str. Its test results are identical to those in the "Using decimal.Decimal" section. In my benchmarks, approaches using Fraction were much slower than those using Decimal.

from fractions import Fraction
import math

def round_nearest(num: float, to: float) -> float:
    num, to = Fraction(str(num)), Fraction(str(to))
    return float(round(num / to) * to)

def round_down(num: float, to: float) -> float:
    num, to = Fraction(str(num)), Fraction(str(to))
    return float(math.floor(num / to) * to)

def round_up(num: float, to: float) -> float:
    num, to = Fraction(str(num)), Fraction(str(to))
    return float(math.ceil(num / to) * to)

Solution 4:[4]

def round_nearest(x, a):
  return round(round(x / a) * a, 2)

It is a slightly different variation.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Paul Hankin
Solution 2
Solution 3
Solution 4 Asclepius