Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

Edit: Latest version download HERE

Looking for bugs. I tried to test all functionality in a variety of ways. Looking forward to feedback.

#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)
share|improve this question
 
Post rolled back, based on comments given in the answer. Please don't update the original code based on answers; this will invalidate them. You may still post the updated code below the original, or start a new question (perhaps with a different focus) if you'd like another review. –  Jamal Nov 16 '13 at 1:02
 
It is also recommended that you make your edits substantial to help avoid making too many of them. As you can see, making too many will trigger Community Wiki. This means that 1.) you'll no longer receive rep from upvotes on this question and 2.) future answers will become CW with the same effect, possibly discouraging others from posting answers. –  Jamal Nov 16 '13 at 1:05
 
Sorry, I meant based on your comments posted under the answer. –  Jamal Nov 16 '13 at 1:09
 
@Jamal That's good to know. I didn't see anything about those rules and recommendations. Perhaps it would be good to put a little something on the "Ask a question" page with info like that. It might also be beneficial to put that kind of info somewhere in the "edit" dialogue. It might cut down on posts that trigger CW, if volume is an issue of concern for CW. I'd like to think that if I'd had some warning about this policy or a popup that says "you've only got 1 edit left before you trigger CW" I would have avoided it. Once it is CW, there is no going back? –  JB0x2D1 Nov 16 '13 at 2:56
 
I understand. As this is something Stack Exchange-wide, any info on it would be found on Meta Stack Overflow. But, I think it could be implemented on CR individually. You could request this feature on Meta Code Review and wait for a moderator to respond (only they can make these kinds of changes). –  Jamal Nov 16 '13 at 3:01
add comment

2 Answers

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.

share|improve this answer
 
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? –  JB0x2D1 Nov 13 '13 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. –  JB0x2D1 Nov 13 '13 at 18:12
1  
What's wrong with 29 + Fraction(9, 16)? –  Gareth Rees Nov 13 '13 at 18:18
1  
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. –  JB0x2D1 Nov 13 '13 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. –  JB0x2D1 Nov 13 '13 at 19:30
show 1 more comment

My goal is to expand the functionality of the standard library Fraction class to accept anything that Fraction would and more: Mixed('3 4/5'), Mixed(3,4,5), Mixed(Fraction(19,5)), etc. Looking Looking for bugs. I tried to test all functionality in a variety of ways. Looking forward to feedback.

#Mixed Fraction Class 1.0.2  15NOV13
#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)
        >>> 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(Mixed, cls).__new__(cls)

        flags = 0
        ATTEMPT_FAILED = 1
        ZERO_DIV = 2

        if denominator is None: #if two arguments or less, pass to Fraction
            try:
                f1 = Fraction(0)
                f2 = Fraction(whole, numerator)                
            except ValueError: #Fraction creation from args failed
                flags |= ATTEMPT_FAILED
                pass
            except ZeroDivisionError:
                #override Fraction ZeroDivisionError with our own
                flags |= ZERO_DIV
                pass
            if flags & ZERO_DIV:
                raise ZeroDivisionError('Mixed(%s, 0)' % whole)
            if flags & ATTEMPT_FAILED: 
                #if str, split and pass to Fraction
                if (numerator is None) and isinstance(whole, str):
                    n = whole.split()
                    if (len(n) == 2):
                        try:
                            f1 = Fraction(n[0])
                            f2 = Fraction(n[1])
                            flags &= ~ATTEMPT_FAILED
                        except ValueError:
                            #override Fraction ValueError with our own
                            flags |= ATTEMPT_FAILED
                            pass
                    else: #split string items != 2 therefore invalid
                        flags |= ATTEMPT_FAILED
            if flags & ATTEMPT_FAILED:
                raise ValueError('Invalid literal for Mixed: %r' %
                                         whole)
        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))
            f1 = Fraction(whole)
            f2 = Fraction(numerator, denominator)
        else:
            raise TypeError("all three arguments should be "
                            "Rational instances")
        #handle negatives and consolidate terms into numerator/denominator
        if (f1 < 0) and (f2 > 0):
            f2 = -f2 + f1
        elif (f1 > 0) and (f2 < 0):
            f2 += -f1
        else:
            f2 += f1
        self._numerator = f2.numerator
        self._denominator = f2.denominator
        return self

    def __repr__(self):
        """repr(self)"""
        if (self._numerator < 0) and (self.whole !=0):
            return ('Mixed(%s, %s, %s)' % (self.whole, -self.fnumerator,
                                           self._denominator))        
        else:
            return ('Mixed(%s, %s, %s)' % (self.whole, self.fnumerator,
                                           self._denominator))

    def __str__(self):
        """str(self)"""
        if self.fnumerator == 0:
            return str(self.whole)
        elif (self._numerator < 0) and (self.whole != 0):
            return '%s %s/%s' % (self.whole, -self.fnumerator,
                                 self._denominator)
        elif self.whole != 0:
            return '%s %s/%s' % (self.whole, self.fnumerator,
                                 self._denominator)
        else:
            return '%s/%s' % (self.fnumerator, self._denominator)

    def to_fraction(self):
        n = self.fnumerator
        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):
        """Fraction(a).numerator
        e.g. Mixed(1,2,3).numerator ==> Fraction(5,2).numerator
        >>> Mixed(1,2,3).numerator 
        5
        """
        return a._numerator

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

    @property
    def whole(a):
        """a % 1
        returns the whole number only
        >>> Mixed(10,3).whole
        3
        """
        if a._numerator < 0:
            return -(-a._numerator // a._denominator)
        else:
            return a._numerator // a._denominator

    @property
    def fnumerator(a):
        """ returns the fractional portion's numerator.
        >>> Mixed('1 3/4').fnumerator
        3
        """
        if a._numerator < 0:
            return -(-a._numerator % a._denominator)
        else:
            return a._numerator % a._denominator

    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)
share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.