4
\$\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.

\$\endgroup\$
0

2 Answers 2

2
\$\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.

\$\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\$
    – JB0x2D1
    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\$
    – JB0x2D1
    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\$
    – JB0x2D1
    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\$
    – JB0x2D1
    Commented Nov 13, 2013 at 19:30
1
\$\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
\$\endgroup\$

Your Answer

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

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