5
\$\begingroup\$

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
 def _add(a, b):
 """a + b"""
 return Mixed(a.numerator * b.denominator +
 b.numerator * a.denominator,
 a.denominator * b.denominator)
 __add__, __radd__ = Fraction._operator_fallbacks(_add, operator.add)
 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)

Latest version download here.

Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Nov 13, 2013 at 4:19
\$\endgroup\$
0

2 Answers 2

3
\$\begingroup\$

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.

answered Nov 13, 2013 at 11:53
\$\endgroup\$
6
  • \$\begingroup\$ 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? \$\endgroup\$ Commented Nov 13, 2013 at 16:41
  • \$\begingroup\$ 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. \$\endgroup\$ Commented Nov 13, 2013 at 18:12
  • 1
    \$\begingroup\$ What's wrong with 29 + Fraction(9, 16)? \$\endgroup\$ Commented Nov 13, 2013 at 18:18
  • 1
    \$\begingroup\$ 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. \$\endgroup\$ Commented Nov 13, 2013 at 19:17
  • \$\begingroup\$ 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. \$\endgroup\$ Commented Nov 13, 2013 at 19:30
2
\$\begingroup\$

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
answered Jul 14, 2015 at 8:06
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.