Python 将浮点数四舍五入到最接近的 0.05 或另一个浮点数的倍数

问题描述 投票:0回答:5

我想模拟这个功能。我想将浮点数舍入到最接近的 0.05 倍数(或者通常舍入到最接近的倍数)。

我想要这个:

>>> round_nearest(1.29, 0.05)
1.25

>>> round_nearest(1.30, 0.05)
1.30

我能做到:

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.

上面的错误答案可能是由于浮点舍入造成的。我可以进行一些特殊情况检查,看看

n
是否“足够接近”
r
的倍数而不进行减法,这可能会起作用,但是有更好的方法吗?或者这个策略是最好的选择吗?

python floating-point rounding rounding-error
5个回答
37
投票

您可以向下舍入到最接近的

a
倍数,如下所示:

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

您可以四舍五入到最接近的

a
倍数,如下所示:

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

25
投票

正如Paul所写:

您可以像这样四舍五入到最接近的倍数:

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

几乎完美地工作,但是

round_nearest(1.39, 0.05)
给出了1.4000000000000001。 为了避免这种情况,我建议这样做:

import math

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

四舍五入到精度

a
,然后四舍五入到有效位数,这就是你的精度
a

编辑

正如@Asclepius所示,此代码对精度的第一个数字有限制(这意味着,例如,如果您输入 4.3,则四舍五入到最接近的整数,如果您输入 0.25,那么数字毕竟四舍五入到第一个小数位。这很容易通过查找精度实际包含多少位数字来修复,然后四舍五入到这个数字:

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
是精度的 log10 舍入(最接近的数字),因此它基本上显示应考虑多少小数位(或者在数字更大的情况下 - 整数位)。因此,如果您的精度为 0.25,那么
frac_digits
将等于 2,因为有 2 个小数位。如果您的精度为 40,则
frac_digits
将等于 -1,因为您需要从小数分隔符“返回”一位数字。


7
投票

Paul之前的回答未通过测试

round_down(4.6, 0.2) == 4.6

这个答案有两种类型的解决方案,inexactexact。他们通过了之前的所有测试以及更多测试,结果也是负数。每种方法都提供了针对

round_nearest
round_down
round_up
的解决方案。

作为免责声明,这些解决方案需要进行更多测试。如果使用

math.isclose
,则应用其默认容差。

你能找到一个失败的例子吗?

要设计其他精确解决方案,请考虑此参考资料

使用
round
(不精确)

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)

使用
math.fmod
(不精确)

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

使用
math.remainder
(不精确)

本节仅实现

round_nearest
。对于
round_down
round_up
,请使用与“使用
round
”部分中相同的逻辑。

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

使用
decimal.Decimal
(准确)

请注意,这是一个低效的解决方案,因为它使用

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)

使用
fractions.Fraction
(准确)

请注意,这是一个低效的解决方案,因为它使用了

str
。其测试结果与“使用
decimal.Decimal
”部分中的结果相同。在我的基准测试中,使用
Fraction
的方法比使用
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)

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

这是一个略有不同的变体。


0
投票

我认为最简单的答案是

a= 13.9
b=round(a/0.05)*0.05
b=round(b,2)
© www.soinside.com 2019 - 2024. All rights reserved.