ApproxScalar: Reduce float rounding errors

This commit reduces float rounding errors when comparing numbers with
a high (and non-float representable) mantissa and a low tolerance.

This came up when comparing geo coordinates with pytest.approx, e.g.:
>>> tolerance = 5e-7
>>> expected = 12.3456785
>>> actual = 12.345678
>>> abs(expected - actual) <= tolerance
False
>>> expected - tolerance <= actual <= expected + tolerance
True

versus

>>> expected = 51.500744
>>> actual = 51.5007435
>>> abs(expected - actual) <= tolerance
True
>>> expected - tolerance <= actual <= expected + tolerance
True
This commit is contained in:
Fabian Henze 2021-12-01 12:42:26 +00:00
parent e2ee3144ed
commit 1a948f4f21
2 changed files with 21 additions and 1 deletions

View File

@ -3,6 +3,7 @@ import pprint
from collections.abc import Sized from collections.abc import Sized
from decimal import Decimal from decimal import Decimal
from numbers import Complex from numbers import Complex
from numbers import Real
from types import TracebackType from types import TracebackType
from typing import Any from typing import Any
from typing import Callable from typing import Callable
@ -453,7 +454,16 @@ class ApproxScalar(ApproxBase):
return False return False
# Return true if the two numbers are within the tolerance. # Return true if the two numbers are within the tolerance.
result: bool = abs(self.expected - actual) <= self.tolerance result: bool
if isinstance(self.expected, Real) and isinstance(actual, Real):
# Use three-way comparison instead of abs() to reduce float rounding errors.
result = (
self.expected - self.tolerance
<= actual
<= self.expected + self.tolerance
)
else:
result = abs(self.expected - actual) <= self.tolerance
return result return result
# Ignore type because of https://github.com/python/mypy/issues/4266. # Ignore type because of https://github.com/python/mypy/issues/4266.

View File

@ -414,6 +414,16 @@ class TestApprox:
# than have a small amount of floating-point error. # than have a small amount of floating-point error.
assert 0.1 + 0.2 == approx(0.3) assert 0.1 + 0.2 == approx(0.3)
@pytest.mark.parametrize(
("expected", "actual", "abs_tolerance"),
[
(12.3456785, 12.345678, 5e-7),
(51.500744, 51.5007435, 5e-7),
],
)
def test_float_rounding(self, expected, actual, abs_tolerance):
assert actual == pytest.approx(expected, abs=abs_tolerance)
def test_default_tolerances(self): def test_default_tolerances(self):
# This tests the defaults as they are currently set. If you change the # This tests the defaults as they are currently set. If you change the
# defaults, this test will fail but you should feel free to change it. # defaults, this test will fail but you should feel free to change it.