Skip to content

Commit

Permalink
Speed up the internal creation of result Fractions if the calculated …
Browse files Browse the repository at this point in the history
…numerator/denominator pair is already normalised.

Follows python/cpython#101780
  • Loading branch information
scoder committed Mar 19, 2023
1 parent 475b58b commit c5b6a2e
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 27 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ ChangeLog
* ``Fraction.limit_denominator()`` is faster, following
https://github.com/python/cpython/pull/93730

* Internal creation of result Fractions is about 10% faster if the calculated
numerator/denominator pair is already normalised, following
https://github.com/python/cpython/pull/101780


1.13 (2022-01-11)
-----------------

Expand Down
85 changes: 58 additions & 27 deletions src/quicktions.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ cdef object _FLOAT_FORMAT_SPECIFICATION_MATCHER = re.compile(r"""
$
""", re.DOTALL | re.VERBOSE).match

cdef object NOINIT = object()


cdef class Fraction:
"""A Rational number.
Expand Down Expand Up @@ -367,9 +369,12 @@ cdef class Fraction:
cdef _denominator
cdef Py_hash_t _hash

def __cinit__(self, numerator=0, denominator=None, *, bint _normalize=True):
cdef Fraction value
def __cinit__(self, numerator=0, denominator=None):
self._hash = -1
if numerator is NOINIT:
return # fast-path for external initialisation

cdef bint _normalize = True
if denominator is None:
if type(numerator) is int or type(numerator) is long:
self._numerator = numerator
Expand Down Expand Up @@ -500,14 +505,19 @@ cdef class Fraction:
raise OverflowError(f"Cannot convert {dec} to {cls.__name__}.")
if dec.is_nan():
raise ValueError(f"Cannot convert {dec} to {cls.__name__}.")

if _decimal_supports_integer_ratio:
num, denom = dec.as_integer_ratio()
return _fraction_from_coprime_ints(num, denom, cls)

sign, digits, exp = dec.as_tuple()
digits = int(''.join(map(str, digits)))
if sign:
digits = -digits
if exp >= 0:
return cls(digits * pow10(exp))
return _fraction_from_coprime_ints(digits * pow10(exp), None, cls)
else:
return cls(digits, pow10(-exp))
return _fraction_from_coprime_ints(digits, pow10(-exp), cls)

def is_integer(self):
"""Return True if the Fraction is an integer."""
Expand Down Expand Up @@ -574,9 +584,9 @@ cdef class Fraction:
# the distance from p1/q1 to self is d/(q1*self._denominator). So we
# need to compare 2*(q0+k*q1) with self._denominator/d.
if 2*d*(q0+k*q1) <= self._denominator:
return Fraction(p1, q1, _normalize=False)
return _fraction_from_coprime_ints(p1, q1)
else:
return Fraction(p0+k*p1, q0+k*q1, _normalize=False)
return _fraction_from_coprime_ints(p0+k*p1, q0+k*q1)

@property
def numerator(self):
Expand Down Expand Up @@ -839,15 +849,15 @@ cdef class Fraction:
"""+a: Coerces a subclass instance to Fraction"""
if type(a) is Fraction:
return a
return Fraction(a._numerator, a._denominator, _normalize=False)
return _fraction_from_coprime_ints(a._numerator, a._denominator)

def __neg__(a):
"""-a"""
return Fraction(-a._numerator, a._denominator, _normalize=False)
return _fraction_from_coprime_ints(-a._numerator, a._denominator)

def __abs__(a):
"""abs(a)"""
return Fraction(abs(a._numerator), a._denominator, _normalize=False)
return _fraction_from_coprime_ints(abs(a._numerator), a._denominator)

def __int__(a):
"""int(a)"""
Expand Down Expand Up @@ -1113,20 +1123,39 @@ cdef class Fraction:
Rational.register(Fraction)


cdef _fraction_from_coprime_ints(numerator, denominator, cls=None):
"""Convert a pair of ints to a rational number, for internal use.
The ratio of integers should be in lowest terms and the denominator
should be positive.
"""
cdef Fraction obj
if cls is None or cls is Fraction:
obj = Fraction.__new__(Fraction, NOINIT, NOINIT)
else:
obj = super(Fraction, cls).__new__(cls)
obj._numerator = numerator
obj._denominator = denominator
return obj


cdef _pow(an, ad, bn, bd):
if bd == 1:
# power = bn
if bn >= 0:
return Fraction(an ** bn,
ad ** bn,
_normalize=False)
elif an >= 0:
return Fraction(ad ** -bn,
an ** -bn,
_normalize=False)
return _fraction_from_coprime_ints(
an ** bn,
ad ** bn)
elif an > 0:
return _fraction_from_coprime_ints(
ad ** -bn,
an ** -bn)
elif an == 0:
raise ZeroDivisionError(f'Fraction({ad ** -bn}, 0)')
else:
return Fraction((-ad) ** -bn,
(-an) ** -bn,
_normalize=False)
return _fraction_from_coprime_ints(
(-ad) ** -bn,
(-an) ** -bn)
else:
# A fractional power will generally produce an
# irrational number.
Expand Down Expand Up @@ -1210,26 +1239,26 @@ cdef _add(na, da, nb, db):
# return Fraction(na * db + nb * da, da * db)
g = _gcd(da, db)
if g == 1:
return Fraction(na * db + da * nb, da * db, _normalize=False)
return _fraction_from_coprime_ints(na * db + da * nb, da * db)
s = da // g
t = na * (db // g) + nb * s
g2 = _gcd(t, g)
if g2 == 1:
return Fraction(t, s * db, _normalize=False)
return Fraction(t // g2, s * (db // g2), _normalize=False)
return _fraction_from_coprime_ints(t, s * db)
return _fraction_from_coprime_ints(t // g2, s * (db // g2))

cdef _sub(na, da, nb, db):
"""a - b"""
# return Fraction(na * db - nb * da, da * db)
g = _gcd(da, db)
if g == 1:
return Fraction(na * db - da * nb, da * db, _normalize=False)
return _fraction_from_coprime_ints(na * db - da * nb, da * db)
s = da // g
t = na * (db // g) - nb * s
g2 = _gcd(t, g)
if g2 == 1:
return Fraction(t, s * db, _normalize=False)
return Fraction(t // g2, s * (db // g2), _normalize=False)
return _fraction_from_coprime_ints(t, s * db)
return _fraction_from_coprime_ints(t // g2, s * (db // g2))

cdef _mul(na, da, nb, db):
"""a * b"""
Expand All @@ -1242,12 +1271,14 @@ cdef _mul(na, da, nb, db):
if g2 > 1:
nb //= g2
da //= g2
return Fraction(na * nb, db * da, _normalize=False)
return _fraction_from_coprime_ints(na * nb, db * da)

cdef _div(na, da, nb, db):
"""a / b"""
# return Fraction(na * db, da * nb)
# Same as _mul(), with inversed b.
if nb == 0:
raise ZeroDivisionError(f'Fraction({db}, 0)')
g1 = _gcd(na, nb)
if g1 > 1:
na //= g1
Expand All @@ -1259,7 +1290,7 @@ cdef _div(na, da, nb, db):
n, d = na * db, nb * da
if d < 0:
n, d = -n, -d
return Fraction(n, d, _normalize=False)
return _fraction_from_coprime_ints(n, d)

cdef _floordiv(an, ad, bn, bd):
"""a // b -> int"""
Expand Down
1 change: 1 addition & 0 deletions src/test_fractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,7 @@ def testArithmetic(self):
self.assertEqual(F(5, 6), F(2, 3) * F(5, 4))
self.assertEqual(F(1, 4), F(1, 10) / F(2, 5))
self.assertEqual(F(-15, 8), F(3, 4) / F(-2, 5))
self.assertRaises(ZeroDivisionError, operator.truediv, F(1), F(0))
self.assertTypedEquals(2, F(9, 10) // F(2, 5))
self.assertTypedEquals(10**23, F(10**23, 1) // F(1))
self.assertEqual(F(5, 6), F(7, 3) % F(3, 2))
Expand Down

0 comments on commit c5b6a2e

Please sign in to comment.