Skip to content

Commit b52c730

Browse files
gh-121797: Add class method Fraction.from_number() (GH-121800)
It is an alternative constructor which only accepts a single numeric argument. Unlike to Fraction.from_float() and Fraction.from_decimal() it accepts any real numbers supported by the standard constructor (int, float, Decimal, Rational numbers, objects with as_integer_ratio()). Unlike to the standard constructor, it does not accept strings.
1 parent 66b3922 commit b52c730

File tree

5 files changed

+82
-8
lines changed

5 files changed

+82
-8
lines changed

Doc/library/fractions.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,16 @@ another rational number, or from a string.
166166
instance.
167167

168168

169+
.. classmethod:: from_number(number)
170+
171+
Alternative constructor which only accepts instances of
172+
:class:`numbers.Integral`, :class:`numbers.Rational`,
173+
:class:`float` or :class:`decimal.Decimal`, and objects with
174+
the :meth:`!as_integer_ratio` method, but not strings.
175+
176+
.. versionadded:: 3.14
177+
178+
169179
.. method:: limit_denominator(max_denominator=1000000)
170180

171181
Finds and returns the closest :class:`Fraction` to ``self`` that has

Doc/whatsnew/3.14.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ fractions
263263
:meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.
264264
(Contributed by Serhiy Storchaka in :gh:`82017`.)
265265

266+
* Add alternative :class:`~fractions.Fraction` constructor
267+
:meth:`Fraction.from_number() <fractions.Fraction.from_number>`.
268+
(Contributed by Serhiy Storchaka in :gh:`121797`.)
269+
266270

267271
functools
268272
---------

Lib/fractions.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,8 @@ def __new__(cls, numerator=0, denominator=None):
279279
numerator = -numerator
280280

281281
else:
282-
raise TypeError("argument should be a string or a number")
282+
raise TypeError("argument should be a string or a Rational "
283+
"instance or have the as_integer_ratio() method")
283284

284285
elif type(numerator) is int is type(denominator):
285286
pass # *very* normal case
@@ -305,6 +306,28 @@ def __new__(cls, numerator=0, denominator=None):
305306
self._denominator = denominator
306307
return self
307308

309+
@classmethod
310+
def from_number(cls, number):
311+
"""Converts a finite real number to a rational number, exactly.
312+
313+
Beware that Fraction.from_number(0.3) != Fraction(3, 10).
314+
315+
"""
316+
if type(number) is int:
317+
return cls._from_coprime_ints(number, 1)
318+
319+
elif isinstance(number, numbers.Rational):
320+
return cls._from_coprime_ints(number.numerator, number.denominator)
321+
322+
elif (isinstance(number, float) or
323+
(not isinstance(number, type) and
324+
hasattr(number, 'as_integer_ratio'))):
325+
return cls._from_coprime_ints(*number.as_integer_ratio())
326+
327+
else:
328+
raise TypeError("argument should be a Rational instance or "
329+
"have the as_integer_ratio() method")
330+
308331
@classmethod
309332
def from_float(cls, f):
310333
"""Converts a finite float to a rational number, exactly.

Lib/test/test_fractions.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,13 @@ def __repr__(self):
283283
class RectComplex(Rect, complex):
284284
pass
285285

286+
class Ratio:
287+
def __init__(self, ratio):
288+
self._ratio = ratio
289+
def as_integer_ratio(self):
290+
return self._ratio
291+
292+
286293
class FractionTest(unittest.TestCase):
287294

288295
def assertTypedEquals(self, expected, actual):
@@ -355,14 +362,9 @@ def testInitFromDecimal(self):
355362
self.assertRaises(OverflowError, F, Decimal('-inf'))
356363

357364
def testInitFromIntegerRatio(self):
358-
class Ratio:
359-
def __init__(self, ratio):
360-
self._ratio = ratio
361-
def as_integer_ratio(self):
362-
return self._ratio
363-
364365
self.assertEqual((7, 3), _components(F(Ratio((7, 3)))))
365-
errmsg = "argument should be a string or a number"
366+
errmsg = (r"argument should be a string or a Rational instance or "
367+
r"have the as_integer_ratio\(\) method")
366368
# the type also has an "as_integer_ratio" attribute.
367369
self.assertRaisesRegex(TypeError, errmsg, F, Ratio)
368370
# bad ratio
@@ -388,6 +390,8 @@ class B(metaclass=M):
388390
pass
389391
self.assertRaisesRegex(TypeError, errmsg, F, B)
390392
self.assertRaisesRegex(TypeError, errmsg, F, B())
393+
self.assertRaises(TypeError, F.from_number, B)
394+
self.assertRaises(TypeError, F.from_number, B())
391395

392396
def testFromString(self):
393397
self.assertEqual((5, 1), _components(F("5")))
@@ -594,6 +598,37 @@ def testFromDecimal(self):
594598
ValueError, "cannot convert NaN to integer ratio",
595599
F.from_decimal, Decimal("snan"))
596600

601+
def testFromNumber(self, cls=F):
602+
def check(arg, numerator, denominator):
603+
f = cls.from_number(arg)
604+
self.assertIs(type(f), cls)
605+
self.assertEqual(f.numerator, numerator)
606+
self.assertEqual(f.denominator, denominator)
607+
608+
check(10, 10, 1)
609+
check(2.5, 5, 2)
610+
check(Decimal('2.5'), 5, 2)
611+
check(F(22, 7), 22, 7)
612+
check(DummyFraction(22, 7), 22, 7)
613+
check(Rat(22, 7), 22, 7)
614+
check(Ratio((22, 7)), 22, 7)
615+
self.assertRaises(TypeError, cls.from_number, 3+4j)
616+
self.assertRaises(TypeError, cls.from_number, '5/2')
617+
self.assertRaises(TypeError, cls.from_number, [])
618+
self.assertRaises(OverflowError, cls.from_number, float('inf'))
619+
self.assertRaises(OverflowError, cls.from_number, Decimal('inf'))
620+
621+
# as_integer_ratio not defined in a class
622+
class A:
623+
pass
624+
a = A()
625+
a.as_integer_ratio = lambda: (9, 5)
626+
check(a, 9, 5)
627+
628+
def testFromNumber_subclass(self):
629+
self.testFromNumber(DummyFraction)
630+
631+
597632
def test_is_integer(self):
598633
self.assertTrue(F(1, 1).is_integer())
599634
self.assertTrue(F(-1, 1).is_integer())
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add alternative :class:`~fractions.Fraction` constructor
2+
:meth:`Fraction.from_number() <fractions.Fraction.from_number>`.

0 commit comments

Comments
 (0)