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

20

我想仿照这个函数。我想将一个浮点数向下舍入到最接近的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的倍数,而不进行减法运算,这可能有效,但是否有更好的方法?还是这种策略是最佳选项?


试一下这个 :) 返回 (n+(r-(n%r))) - Abhilash Cherukat
你希望 my_magical_rounding(1.29, 0.05) 返回什么?1.30 还是 1.25 - dopstar
4个回答

35

您可以按以下方式将其舍入到最接近的多个a

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

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

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

1
这对我似乎不起作用。在Python 3.9.5中,round_down(4.6, 0.2)返回4.4,而我期望它返回4.6 - nimble_ninja
@nimble_ninja,请参考我的答案。它会返回4.6作为round_down(4.6, 0.2)的预期结果。 - Asclepius

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,因为您需要从小数分隔符“往回”一位。


重复使用 a 的配置似乎不明智。例如,round_nearest2(82.0, 4.3) 应该返回 81.7,但它返回了 82.0。 - Asclepius
1
@Asclepius 这是代码精度的限制,我没有考虑到多位数的精度。你可以查看更新后的答案,现在也支持多位数的精度。 - Grysik

7

之前的答案由Paul提供,但在测试round_down(4.6, 0.2) == 4.6时失败。

这个答案有两种解决方案,不精确的精确的。它们通过了所有以前的测试,并添加了更多测试,也适用于负数。每个方法都为round_nearestround_downround_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_downround_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)

这是一个稍微不同的变体。


硬编码2使得这个答案无法推广到0.05以外。举个简单的例子,对于round_nearest(.124, .005),它会产生0.12,而正确的答案应该是0.125。 - Asclepius

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接