Skip to content

Commit 03ab8f7

Browse files
authored
Use math.nextafter instead of struct (#42)
This PR updates existing logic for determining nearby floats to use `math.nextafter` instead of `struct`. (`math.nextafter` was only added in Python 3.9, so this change was only possible after dropping Python 3.8 support.)
1 parent 4f580f8 commit 03ab8f7

2 files changed

Lines changed: 35 additions & 11 deletions

File tree

src/simplefractions/__init__.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import fractions
3838
import math
3939
import numbers
40-
import struct
4140
import typing
4241

4342
from simplefractions._simplest_in_interval import _simplest_in_interval
@@ -115,6 +114,11 @@ def _interval_rounding_to(
115114
"""
116115
Return the interval of numbers that round to a given float.
117116
117+
Parameters
118+
----------
119+
x
120+
A finite float.
121+
118122
Returns
119123
-------
120124
left, right : fractions.Fraction
@@ -123,22 +127,18 @@ def _interval_rounding_to(
123127
closed : bool
124128
True if the interval is closed at both ends, else False.
125129
"""
126-
if x < 0:
130+
if x < 0.0:
127131
left, right, closed = _interval_rounding_to(-x)
128132
return -right, -left, closed
129133

130-
if x == 0:
131-
n = struct.unpack("<Q", struct.pack("<d", 0.0))[0]
132-
x_plus = struct.unpack("<d", struct.pack("<Q", n + 1))[0]
133-
right = (fractions.Fraction(x) + fractions.Fraction(x_plus)) / 2
134+
if x == 0.0:
135+
right = fractions.Fraction(math.nextafter(0.0, math.inf)) / 2
134136
return -right, right, True
135137

136-
n = struct.unpack("<Q", struct.pack("<d", x))[0]
137-
x_plus = struct.unpack("<d", struct.pack("<Q", n + 1))[0]
138-
x_minus = struct.unpack("<d", struct.pack("<Q", n - 1))[0]
139-
140-
closed = n % 2 == 0
138+
x_plus = math.nextafter(x, math.inf)
139+
x_minus = math.nextafter(x, 0.0)
141140
left = (fractions.Fraction(x) + fractions.Fraction(x_minus)) / 2
141+
closed = float(left) == x
142142
if math.isinf(x_plus):
143143
# Corner case where x was the largest representable finite float
144144
right = 2 * fractions.Fraction(x) - left

src/simplefractions/test/test_simplefractions.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,30 @@ def test_simplest_from_float(self) -> None:
244244
with self.subTest(f=f):
245245
self.check_simplest_from_float(f)
246246

247+
def test_simplest_from_float_open_closed_cases(self) -> None:
248+
# Cases where it matters whether the interval rounding to
249+
# the float is open or closed.
250+
251+
# Even floats
252+
self.assertEqual(simplest_from_float(1e16), 10**16 - 1)
253+
self.assertEqual(simplest_from_float(1e16 + 4), 10**16 + 3)
254+
self.assertEqual(simplest_from_float(float(2**54 - 4)), 2**54 - 5)
255+
self.assertEqual(simplest_from_float(float(2**54)), 2**54 - 1)
256+
self.assertEqual(simplest_from_float(float(2**54 + 8)), 2**54 + 6)
257+
258+
# Odd floats
259+
self.assertEqual(simplest_from_float(1e16 + 2), 10**16 + 2)
260+
self.assertEqual(simplest_from_float(float(2**54 - 2)), 2**54 - 2)
261+
self.assertEqual(simplest_from_float(float(2**54 + 4)), 2**54 + 3)
262+
263+
def test_simplest_from_float_powers_of_two(self) -> None:
264+
# Powers of two that are exactly representable in IEEE 754 binary64.
265+
TWO = fractions.Fraction(2)
266+
for e in range(-1074, 1024):
267+
f = TWO**e
268+
with self.subTest(f=f):
269+
self.check_simplest_from_float(f)
270+
247271
def test_simplest_from_float_special_values(self) -> None:
248272
with self.assertRaises(ValueError):
249273
simplest_from_float(math.inf)

0 commit comments

Comments
 (0)