# Mixed number fractions class

I'm looking for bugs. I tried to test all functionality in a variety of ways.

#by JB0x2D1

from decimal import Decimal
import math
import numbers
import operator
from fractions import Fraction

class Mixed(Fraction):
"""This class implements Fraction, which implements rational numbers."""
# We're immutable, so use __new__ not __init__
def __new__(cls, whole=0, numerator=None, denominator=None):
"""Constructs a Rational.

Takes a string like '-1 2/3' or '1.5', another Rational instance, a
numerator/denominator pair, a float, or a whole number/numerator/
denominator set.  If one or more non-zero arguments is negative,
all are treated as negative and the result is negative.

General behavior:  whole number + (numerator / denominator)

Examples
--------

>>> Mixed(Mixed(-1,1,2), Mixed(0,1,2), Mixed(0,1,2))
Mixed(-2, 1, 2)
Note: The above call is similar to:
>>> Fraction(-3,2) + Fraction(Fraction(-1,2), Fraction(1,2))
Fraction(-5, 2)
>>> Mixed('-1 2/3')
Mixed(-1, 2, 3)
>>> Mixed(10,-8)
Mixed(-1, 1, 4)
>>> Mixed(Fraction(1,7), 5)
Mixed(0, 1, 35)
>>> Mixed(Mixed(1, 7), Fraction(2, 3))
Mixed(0, 3, 14)
>>> Mixed(Mixed(0, 3, 2), Fraction(2, 3), 2)
Mixed(1, 5, 6)
>>> Mixed('314')
Mixed(314, 0, 1)
>>> Mixed('-35/4')
Mixed(-8, 3, 4)
>>> Mixed('3.1415')
Mixed(3, 283, 2000)
>>> Mixed('-47e-2')
Mixed(0, -47, 100)
>>> Mixed(1.47)
Mixed(1, 2116691824864133, 4503599627370496)
>>> Mixed(2.25)
Mixed(2, 1, 4)
>>> Mixed(Decimal('1.47'))
Mixed(1, 47, 100)

"""
self = super(Fraction, cls).__new__(cls)

if (numerator is None) and (denominator is None): #single argument
if isinstance(whole, numbers.Rational) or \
isinstance(whole, float) or \
isinstance(whole, Decimal):
if type(whole) == Mixed:
return whole
f = Fraction(whole)
whole = 0
elif isinstance(whole, str):
# Handle construction from strings.
arg = whole
fail = False
try:
f = Fraction(whole)
whole = 0
except ValueError:
n = whole.split()
if (len(n) == 2):
try:
whole = Fraction(n[0])
f = Fraction(n[1])
except ValueError:
fail = True
else:
fail = True
if fail:
raise ValueError('Invalid literal for Mixed: %r' %
arg)
else:
raise TypeError("argument should be a string "
"or a Rational instance")
elif (isinstance(numerator, numbers.Rational) and #two arguments
isinstance(whole, numbers.Rational) and (denominator is None)):
#here whole is treated as numerator and numerator as denominator
if numerator == 0:
raise ZeroDivisionError('Mixed(%s, 0)' % whole)
f = Fraction(whole, numerator)
whole = 0
elif (isinstance(whole, numbers.Rational) and #three arguments
isinstance(numerator, numbers.Rational) and
isinstance(denominator, numbers.Rational)):
if denominator == 0:
raise ZeroDivisionError('Mixed(%s, %s, 0)' % whole, numerator)
whole = Fraction(whole)
f = Fraction(numerator, denominator)
else:
raise TypeError("all three arguments should be "
"Rational instances")
#handle negative values and convert improper to mixed number fraction
if (whole < 0) and (f > 0):
f = -f + whole
elif (whole > 0) and (f < 0):
f += -whole
else:
f += whole
numerator = f.numerator
denominator = f.denominator
if numerator < 0:
whole = -(-numerator // denominator)
numerator = -numerator % denominator
else:
whole = numerator // denominator
numerator %= denominator
self._whole = whole
self._numerator = numerator
self._denominator = denominator
return self

def __repr__(self):
"""repr(self)"""
return ('Mixed(%s, %s, %s)' % (self._whole, self._numerator,
self._denominator))

def __str__(self):
"""str(self)"""
if self._numerator == 0:
return str(self._whole)
elif self._whole != 0:
return '%s %s/%s' % (self._whole, self._numerator,
self._denominator)
else:
return '%s/%s' % (self._numerator, self._denominator)

def to_fraction(self):
n = self._numerator
if self._whole != 0:
if self._whole < 0:
n *= -1
n += self._whole * self._denominator
return Fraction(n, self._denominator)

def limit_denominator(self, max_denominator=1000000):
"""Closest Fraction to self with denominator at most max_denominator.

>>> Mixed('3.141592653589793').limit_denominator(10)
Mixed(3, 1, 7)
>>> Mixed('3.141592653589793').limit_denominator(100)
Mixed(3, 14, 99)
>>> Mixed(4321, 8765).limit_denominator(10000)
Mixed(0, 4321, 8765)
"""
return Mixed(self.to_fraction().limit_denominator(max_denominator))

@property
def numerator(a):
return a.to_fraction().numerator

@property
def denominator(a):
return a._denominator

@property
def whole(a):
"""returns the whole number only (a % 1)

>>> Mixed(10,3).whole
3
"""
return a._whole

@property
def fnumerator(a):
""" returns the fractional portion's numerator.

>>> Mixed('1 3/4').fnumerator
3
"""
return a._numerator

"""a + b"""
return Mixed(a.numerator * b.denominator +
b.numerator * a.denominator,
a.denominator * b.denominator)

def _sub(a, b):
"""a - b"""
return Mixed(a.numerator * b.denominator -
b.numerator * a.denominator,
a.denominator * b.denominator)

__sub__, __rsub__ = Fraction._operator_fallbacks(_sub, operator.sub)

def _mul(a, b):
"""a * b"""
return Mixed(a.numerator * b.numerator, a.denominator * b.denominator)

__mul__, __rmul__ = Fraction._operator_fallbacks(_mul, operator.mul)

def _div(a, b):
"""a / b"""
return Mixed(a.numerator * b.denominator,
a.denominator * b.numerator)

__truediv__, __rtruediv__ = Fraction._operator_fallbacks(_div, operator.truediv)

def __pow__(a, b):
"""a ** b

If b is not an integer, the result will be a float or complex
since roots are generally irrational. If b is an integer, the
result will be rational.

"""
if isinstance(b, numbers.Rational):
if b.denominator == 1:
return Mixed(Fraction(a) ** b)
else:
# A fractional power will generally produce an
# irrational number.
return float(a) ** float(b)
else:
return float(a) ** b

def __rpow__(b, a):
"""a ** b"""
if b._denominator == 1 and b._numerator >= 0:
# If a is an int, keep it that way if possible.
return a ** b.numerator

if isinstance(a, numbers.Rational):
return Mixed(a.numerator, a.denominator) ** b

if b._denominator == 1:
return a ** b.numerator

return a ** float(b)

def __pos__(a):
"""+a: Coerces a subclass instance to Fraction"""
return Mixed(a.numerator, a.denominator)

def __neg__(a):
"""-a"""
return Mixed(-a.numerator, a.denominator)

def __abs__(a):
"""abs(a)"""
return Mixed(abs(a.numerator), a.denominator)

def __trunc__(a):
"""trunc(a)"""
if a.numerator < 0:
return -(-a.numerator // a.denominator)
else:
return a.numerator // a.denominator

def __hash__(self):
"""hash(self)"""
return self.to_fraction().__hash__()

def __eq__(a, b):
"""a == b"""
return Fraction(a) == b

def _richcmp(self, other, op):
"""Helper for comparison operators, for internal use only.

Implement comparison between a Rational instance self, and
either another Rational instance or a float other.  If
other is not a Rational instance or a float, return
NotImplemented. op should be one of the six standard
comparison operators.

"""
return self.to_fraction()._richcmp(other, op)

def __reduce__(self):
return (self.__class__, (str(self),))

def __copy__(self):
if type(self) == Mixed:
return self     # I'm immutable; therefore I am my own clone
return self.__class__(self.numerator, self.denominator)

def __deepcopy__(self, memo):
if type(self) == Mixed:
return self     # My components are also immutable
return self.__class__(self.numerator, self.denominator)


### 1. Bugs

Your doctests do not pass:

\$ python3.3 -mdoctest cr35274.py
**********************************************************************
File "./cr35274.py", line 76, in cr35274.Mixed.__new__
Failed example:
Mixed(Mixed(-1,1,2), Mixed(0,1,2), Mixed(0,1,2))
Expected:
Mixed(-2, 1, 2)
Note: The above call is similar to:
Got:
Mixed(-2, 1, 2)
**********************************************************************
File "./cr35274.py", line 97, in cr35274.Mixed.__new__
Failed example:
Mixed('-47e-2')
Expected:
Mixed(0, -47, 100)
Got:
Mixed(0, 47, 100)
**********************************************************************
1 items had failures:
2 of  14 in cr35274.Mixed.__new__
***Test Failed*** 2 failures.


### 2. Commentary

As far as I can see, there are really only two things that you are trying to achieve:

1. To create the mixed fraction a b/c from the string "a b/c". But instead of implementing a whole new class, why not just write a function to parse the string and return a Fraction?

import re
from fractions import Fraction

_MIXED_FORMAT = re.compile(r"""
\A\s*                      # optional whitespace at the start, then
(?P<sign>[-+]?)            # an optional sign, then
(?P<whole>\d+)             # integer part
\s+                        # whitespace
(?P<num>\d+)               # numerator
/(?P<denom>\d+)            # denominator
\s*\Z                      # and optional whitespace to finish
""", re.VERBOSE)

def mixed(s):
"""Parse the string s as a (possibly mixed) fraction.

>>> mixed('1 2/3')
Fraction(5, 3)
>>> mixed(' -1 2/3 ')
Fraction(-5, 3)
>>> mixed('-0  12/15')
Fraction(-4, 5)
>>> mixed('+45/15')
Fraction(3, 1)

"""
m = _MIXED_FORMAT.match(s)
if not m:
return Fraction(s)
d = m.groupdict()
result = int(d['whole']) + Fraction(int(d['num']), int(d['denom']))
if d['sign'] == '-':
return -result
else:
return result

2. To format a fraction in mixed notation. But why not just write this as a function:

def format_mixed(f):
"""Format the fraction f as a (possibly) mixed fraction.

>>> all(format_mixed(mixed(f)) == f for f in ['1 2/3', '-3 4/5', '7/8'])
True

"""
if abs(f) <= 1 or f.denominator == 1:
return str(f)
return '{0} {1.numerator}/{1.denominator}'.format(int(f), abs(f - int(f)))


The rest of your code seems unnecessary and complicated.

• Thanks for the bug (fixed). I chose to implement it as a class to make it easy to represent any rational or float (or string representation of either) as a mixed number. In short, if I'm going to bother to handle strings, why stop there? Commented Nov 13, 2013 at 16:41
• To me, it is much more convenient if I want to manipulate the value 29 9/16 to use Mixed(29,9,16) instead of Fraction((29*16)+9,16). That is why I wrote the full blown class. Even if I wanted a Fraction type to manipulate, I could use Fraction(Mixed(29,9,16)) for convenience and let Python figure out the details. Commented Nov 13, 2013 at 18:12
• What's wrong with 29 + Fraction(9, 16)? Commented Nov 13, 2013 at 18:18
• Nothing at all. This class is written to add convenience. Mixed does everything that Fraction does and a bit more. Not to mention that >>>print(mix) 29 9/16 is a little more meaningful to me than >>>print(frac) 473/16. Functionally, they are more or less interchangeable. Commented Nov 13, 2013 at 19:17
• I guess it comes down to the fact that if one prefers the mixed number style, they can easily implement it without wiring the function to parse the string and return the fraction. They don't have to write the function to display the value in mixed number format. And they can switch back and forth at will, effortlessly. It's already done for you. Commented Nov 13, 2013 at 19:30

Don't ever reinvent the wheel. Fraction is decently cooperative class, you should never have to write basic operations from scratch. Use the inherited stuff. Here is the solution for basic arithmetic, shouldn't be hard to adapt for many other protocols. Also, it doesn't handle negative numbers because I'm lazy, I trust you can add support for that too.

from fractions import Fraction
class Mixed(Fraction):
def __new__(cls, a, b, c):
return super().__new__(cls, a*c + b, c)
def __str__(self):
return '{} {}'.format(*divmod(Fraction(self), 1))
def inject(name, *, namespace = locals()):
name = '__{}__'.format(name)
def method(*args):
result = getattr(Fraction, name)(*args)
return Fraction.__new__(Mixed, result)
namespace[name] = method
for name in 'add sub mul truediv'.split():
inject(name)
inject('r' + name)
for name in 'abs pos neg'.split():
inject(name)
del name, inject
print(Fraction(122,3) / Mixed(4,5,6) + 5)  # 13 12/29