-
-
Notifications
You must be signed in to change notification settings - Fork 32k
bpo-29962: add math.remainder #950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
55f679f
9d50ace
49aaa2a
2b79f77
6ab3fd3
6d80fce
68273b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1000,6 +1000,135 @@ def testRadians(self): | |
self.ftest('radians(-45)', math.radians(-45), -math.pi/4) | ||
self.ftest('radians(0)', math.radians(0), 0) | ||
|
||
@requires_IEEE_754 | ||
def testRemainder(self): | ||
from fractions import Fraction | ||
|
||
def validate_spec(x, y, r): | ||
""" | ||
Check that r matches remainder(x, y) according to the IEEE 754 | ||
specification. Assumes that x, y and r are finite and y is nonzero. | ||
""" | ||
fx, fy, fr = Fraction(x), Fraction(y), Fraction(r) | ||
# r should not exceed y/2 in absolute value | ||
self.assertLessEqual(abs(fr), abs(fy/2)) | ||
# x - r should be an exact integer multiple of y | ||
n = (fx - fr) / fy | ||
self.assertEqual(n, int(n)) | ||
if abs(fr) == abs(fy/2): | ||
# If |r| == |y/2|, n should be even. | ||
self.assertEqual(n/2, int(n/2)) | ||
|
||
# triples (x, y, remainder(x, y)) in hexadecimal form. | ||
testcases = [ | ||
# Remainders modulo 1, showing the ties-to-even behaviour. | ||
'-4.0 1 -0.0', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason for using strings to be splitted instead of a more natural container like tuples? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just personal preference, really. I find the individual cases to be easier to read and write this way. Comparing:
with
there's a lot more noise in the latter representation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Of course, if Python had direct support for hex floating-point literals, then
would be the obvious representation. But it doesn't. :-( |
||
'-3.8 1 0.8', | ||
'-3.0 1 -0.0', | ||
'-2.8 1 -0.8', | ||
'-2.0 1 -0.0', | ||
'-1.8 1 0.8', | ||
'-1.0 1 -0.0', | ||
'-0.8 1 -0.8', | ||
'-0.0 1 -0.0', | ||
' 0.0 1 0.0', | ||
' 0.8 1 0.8', | ||
' 1.0 1 0.0', | ||
' 1.8 1 -0.8', | ||
' 2.0 1 0.0', | ||
' 2.8 1 0.8', | ||
' 3.0 1 0.0', | ||
' 3.8 1 -0.8', | ||
' 4.0 1 0.0', | ||
|
||
# Reductions modulo 2*pi | ||
'0x0.0p+0 0x1.921fb54442d18p+2 0x0.0p+0', | ||
'0x1.921fb54442d18p+0 0x1.921fb54442d18p+2 0x1.921fb54442d18p+0', | ||
'0x1.921fb54442d17p+1 0x1.921fb54442d18p+2 0x1.921fb54442d17p+1', | ||
'0x1.921fb54442d18p+1 0x1.921fb54442d18p+2 0x1.921fb54442d18p+1', | ||
'0x1.921fb54442d19p+1 0x1.921fb54442d18p+2 -0x1.921fb54442d17p+1', | ||
'0x1.921fb54442d17p+2 0x1.921fb54442d18p+2 -0x0.0000000000001p+2', | ||
'0x1.921fb54442d18p+2 0x1.921fb54442d18p+2 0x0p0', | ||
'0x1.921fb54442d19p+2 0x1.921fb54442d18p+2 0x0.0000000000001p+2', | ||
'0x1.2d97c7f3321d1p+3 0x1.921fb54442d18p+2 0x1.921fb54442d14p+1', | ||
'0x1.2d97c7f3321d2p+3 0x1.921fb54442d18p+2 -0x1.921fb54442d18p+1', | ||
'0x1.2d97c7f3321d3p+3 0x1.921fb54442d18p+2 -0x1.921fb54442d14p+1', | ||
'0x1.921fb54442d17p+3 0x1.921fb54442d18p+2 -0x0.0000000000001p+3', | ||
'0x1.921fb54442d18p+3 0x1.921fb54442d18p+2 0x0p0', | ||
'0x1.921fb54442d19p+3 0x1.921fb54442d18p+2 0x0.0000000000001p+3', | ||
'0x1.f6a7a2955385dp+3 0x1.921fb54442d18p+2 0x1.921fb54442d14p+1', | ||
'0x1.f6a7a2955385ep+3 0x1.921fb54442d18p+2 0x1.921fb54442d18p+1', | ||
'0x1.f6a7a2955385fp+3 0x1.921fb54442d18p+2 -0x1.921fb54442d14p+1', | ||
'0x1.1475cc9eedf00p+5 0x1.921fb54442d18p+2 0x1.921fb54442d10p+1', | ||
'0x1.1475cc9eedf01p+5 0x1.921fb54442d18p+2 -0x1.921fb54442d10p+1', | ||
|
||
# Symmetry with respect to signs. | ||
' 1 0.c 0.4', | ||
'-1 0.c -0.4', | ||
' 1 -0.c 0.4', | ||
'-1 -0.c -0.4', | ||
' 1.4 0.c -0.4', | ||
'-1.4 0.c 0.4', | ||
' 1.4 -0.c -0.4', | ||
'-1.4 -0.c 0.4', | ||
|
||
# Huge modulus, to check that the underlying algorithm doesn't | ||
# rely on 2.0 * modulus being representable. | ||
'0x1.dp+1023 0x1.4p+1023 0x0.9p+1023', | ||
'0x1.ep+1023 0x1.4p+1023 -0x0.ap+1023', | ||
'0x1.fp+1023 0x1.4p+1023 -0x0.9p+1023', | ||
] | ||
|
||
for case in testcases: | ||
with self.subTest(case=case): | ||
x_hex, y_hex, expected_hex = case.split() | ||
x = float.fromhex(x_hex) | ||
y = float.fromhex(y_hex) | ||
expected = float.fromhex(expected_hex) | ||
validate_spec(x, y, expected) | ||
actual = math.remainder(x, y) | ||
# Cheap way of checking that the floats are | ||
# as identical as we need them to be. | ||
self.assertEqual(actual.hex(), expected.hex()) | ||
|
||
# Test tiny subnormal modulus: there's potential for | ||
# getting the implementation wrong here (for example, | ||
# by assuming that modulus/2 is exactly representable). | ||
tiny = float.fromhex('1p-1074') # min +ve subnormal | ||
for n in range(-25, 25): | ||
if n == 0: | ||
continue | ||
y = n * tiny | ||
for m in range(100): | ||
x = m * tiny | ||
actual = math.remainder(x, y) | ||
validate_spec(x, y, actual) | ||
actual = math.remainder(-x, y) | ||
validate_spec(-x, y, actual) | ||
|
||
# Special values. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should these cases be moved into tests on their own? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly. I wanted to stay (somewhat) consistent with the existing test structure, which is mostly one test method per wrapped libm function. One day it would be nice to rework |
||
# NaNs should propagate as usual. | ||
for value in [NAN, 0.0, -0.0, 2.0, -2.3, NINF, INF]: | ||
self.assertIsNaN(math.remainder(NAN, value)) | ||
self.assertIsNaN(math.remainder(value, NAN)) | ||
|
||
# remainder(x, inf) is x, for non-nan non-infinite x. | ||
for value in [-2.3, -0.0, 0.0, 2.3]: | ||
self.assertEqual(math.remainder(value, INF), value) | ||
self.assertEqual(math.remainder(value, NINF), value) | ||
|
||
# remainder(x, 0) and remainder(infinity, x) for non-NaN x are invalid | ||
# operations according to IEEE 754-2008 7.2(f), and should raise. | ||
for value in [NINF, -2.3, -0.0, 0.0, 2.3, INF]: | ||
with self.assertRaises(ValueError): | ||
math.remainder(INF, value) | ||
with self.assertRaises(ValueError): | ||
math.remainder(NINF, value) | ||
with self.assertRaises(ValueError): | ||
math.remainder(value, 0.0) | ||
with self.assertRaises(ValueError): | ||
math.remainder(value, -0.0) | ||
|
||
def testSin(self): | ||
self.assertRaises(TypeError, math.sin) | ||
self.ftest('sin(0)', math.sin(0), 0) | ||
|
@@ -1286,6 +1415,12 @@ def test_mtestfile(self): | |
self.fail('Failures in test_mtestfile:\n ' + | ||
'\n '.join(failures)) | ||
|
||
# Custom assertions. | ||
|
||
def assertIsNaN(self, value): | ||
if not math.isnan(value): | ||
self.fail("Expected a NaN, got {!r}.".format(value)) | ||
|
||
|
||
class IsCloseTests(unittest.TestCase): | ||
isclose = math.isclose # sublcasses should override this | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't it be named
remainder_near
asdecimal.remainder_near
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not clear to me: see discussion in the bpo issue.