2013-10-28

Fracciones de Peano

Las últimas 2 semanas estuve haciendo en el laburo el curso de diseño orientado a objetos que da Hernán Wilkinson de 10 Pines. Dentro del curso, hay un ejercicio para hacer single dispatch que consiste en modelar números enteros y fracciones.
Como usar los números que me da el lenguaje para modelar los números me pareció choto, hice una implementación de los números de Peano y con eso hice las fracciones.

Abajo les dejo la primera versión que pasa los tests, pero que es media fea porque tiene ifs de la forma if isinstance(sumando, Enteros): #código. Si miran al final, van a ver los tests de unidad que tiene que pasar el código. Antes está la implementación.


import unittest

class Numero:

    DESCRIPCION_DE_ERROR_DE_DIVISION_POR_CERO = 'No se puede dividir por 0'
   
    def esCero(self):
        self.shouldBeImplementedBySubclass()
   
    def esUno(self):
        self.shouldBeImplementedBySubclass()
   
    def __add__(self,sumando):
        self.shouldBeImplementedBySubclass()
   
    def __mul__(self,factor):
        self.shouldBeImplementedBySubclass()
   
    def __div__(self,divisor):
        self.shouldBeImplementedBySubclass()
   
    def shouldBeImplementedBySubclass(self):
        raise NotImplementedError('Should be implemented by the subclass')
   
    def __le__(self, other):
        return self < other or self == other

class Entero(Numero):
    pass

class Zero(Entero):
   
    def esCero(self): return True
   
    def esUno(self): return False
   
    def __eq__(self, other):
        return self.__class__ == other.__class__
   
    def __add__(self,sumando):
        return sumando
   
    def __mul__(self, factor):
        return self
   
    def __repr__(self): return "0"
   
    def __lt__(self, other): return not self == other

class Next(Entero):

    def __init__(self, before):
        self.before = before
   
    def esCero(self): return False
   
    def esUno(self): return self.before.esCero()
   
    def __add__(self,sumando):
   
        if isinstance(sumando, Entero):
            return Next(self.before + sumando)
       
        return sumando + self
   
    def __mul__(self, factor):
   
        return factor + self.before * factor
   
    def __repr__(self): return "N" + repr(self.before)
   
    def __eq__(self, other):
   
        if isinstance(other, Fraccion):
            return other == self
   
        if self.__class__ != other.__class__: return False

        return self.before == other.before
   
    def __lt__(self, other):
   
        if self.__class__ != other.__class__: return False
        return self.before < other.before
   
    def __div__(self, other):
   
        if isinstance(other, self.__class__):
            return Fraccion(self, other)
   
        if isinstance(other, Fraccion):
            return Fraccion(other.d * self, other.n)
   
        raise Exception("No se puede dividir por %r" % other)

class Fraccion(Numero):

    def __init__(self, n, d):
        if (d.esCero()):
            raise Exception(Numero.DESCRIPCION_DE_ERROR_DE_DIVISION_POR_CERO)
           
        self.n = n
        self.d = d

    def __add__(self, other):

        #a/b + c/d = (a.d + c.b) / (b.d)
   
        if isinstance(other, self.__class__):
            return Fraccion((self.n * other.d + other.n * self.d), self.d * other.d)
       
        if isinstance(other, Entero):
            return Fraccion(self.n + other * self.d, self.d)
       
        raise Exception("No se sumar %r" % other)

    def __mul__(self, other):

        # (a/b) * (c/d) = (a.c) / (b.d)
        if isinstance(other, self.__class__):
            return Fraccion(self.n * other.n, self.d * other.d)
       
        if isinstance(other, Entero):
            return Fraccion(self.n * other, self.d)

    def __div__(self, other):
   
        # (a/b) / (c/d) = (a.d) / (b.c)
       
        if isinstance(other, self.__class__):
            return Fraccion(self.n * other.d, other.n * self.d)
       
        if isinstance(other, Entero):
            return Fraccion(self.n, self.d * other)
       
        raise Exception("No se dividir %r" % other)

    def __eq__(self, other):
   
        if isinstance(other, self.__class__):
            return self.n * other.d == self.d * other.n
       
        if isinstance(other, Entero):
            return self.n == self.d * other
       
        return False

    def __repr__(self): return repr(self.n) + "/" + repr(self.d)

class NumeroTest(unittest.TestCase):

    def createCero(self):
        return Zero()
   
    def createUno(self):
        return Next(self.createCero())
   
    def createDos(self):
        return Next(self.createUno())
   
    def createTres(self):
        return Next(self.createDos())
   
    def createCuatro(self):
        return Next(self.createTres())
   
    def createCinco(self):
        return Next(self.createCuatro())
   
    def createSeis(self):
        return Next(self.createCinco())
   
    def createSiete(self):
        return Next(self.createSeis())
   
    def createUnQuinto(self):
        return Fraccion(self.uno, self.cinco)
   
    def createDosQuintos(self):
        return Fraccion(self.dos, self.cinco)
   
    def createDosVeinticincoavos(self):
        return Fraccion(self.dos, self.cinco * self.cinco)
   
    def createUnMedio(self):
        return Fraccion(self.uno, self.dos)
   
    def createCincoMedios(self):
        return Fraccion(self.cinco, self.dos)
   
    def createSeisQuintos(self):
        return Fraccion(self.seis, self.cinco)
   
    def createCuatroMedios(self):
        return Fraccion(self.cuatro, self.dos)
   
    def createDosCuartos(self):
        return Fraccion(self.dos, self.cuatro)
   
    def setUp(self):
   
        self.cero = self.createCero()
        self.uno = self.createUno()
        self.dos = self.createDos()
        self.tres = self.createTres()
        self.cuatro = self.createCuatro()
        self.cinco = self.createCinco()
        self.seis = self.createSeis()
        self.siete = self.createSiete()
       
        self.unQuinto = self.createUnQuinto()
        self.dosQuintos = self.createDosQuintos()
        self.dosVeinticincoavos = self.createDosVeinticincoavos()
       
        self.unMedio = self.createUnMedio()
        self.cincoMedios = self.createCincoMedios()
        self.seisQuintos = self.createSeisQuintos()
        self.cuatroMedios = self.createCuatroMedios()
        self.dosCuartos = self.createDosCuartos()
   
    def testAEsCeroDevuelveTrueSoloParaElCero(self):
   
        self.assertTrue (self.cero.esCero())
        self.assertFalse (self.uno.esCero())
   
    def testBEsUnoDevuelveTrueSoloParaElUno(self):
   
        self.assertTrue (self.uno.esUno())
        self.assertFalse (self.cero.esUno())
   
    def testCSumaDeEnteros(self):
   
        self.assertEqual (self.dos,self.uno+self.uno)
   
    def testDMultiplicacionDeEnteros(self):
   
        self.assertEqual(self.cuatro, self.dos*self.dos)
       
    def testEDivisionDeEnteros(self):
   
        self.assertEqual(self.uno, self.dos/self.dos)
   
    def testFSumaDeFracciones(self):
   
        sieteDecimos = Fraccion( self.siete, self.dos * self.cinco ) # <- br="" corresponda="" lo="" por="" que="" reemplazar="">       
        self.assertEqual (sieteDecimos,self.unQuinto+self.unMedio)
       
        #
        # La suma de fracciones es:
        #
        # a/b + c/d = (a.d + c.b) / (b.d)
        #
        # SI ESTAN PENSANDO EN LA REDUCCION DE FRACCIONES NO SE PREOCUPEN!
        # NO SE ESTA TESTEANDO ESE CASO
        #
   
    def testGMultiplicacionDeFracciones(self):
   
        self.assertEqual (self.dosVeinticincoavos,self.unQuinto*self.dosQuintos)
       
        #
        # La multiplicacion de fracciones es:
        #
        # (a/b) * (c/d) = (a.c) / (b.d)
        #
        # SI ESTAN PENSANDO EN LA REDUCCION DE FRACCIONES NO SE PREOCUPEN!
        # TODAVIA NO SE ESTA TESTEANDO ESE CASO
        #
   
    def testHDivisionDeFracciones(self):
   
        self.assertEqual (self.cincoMedios,self.unMedio/self.unQuinto)
       
        #
        # La division de fracciones es:
        #
        # (a/b) / (c/d) = (a.d) / (b.c)
        #
        # SI ESTAN PENSANDO EN LA REDUCCION DE FRACCIONES NO SE PREOCUPEN!
        # TODAVIA NO SE ESTA TESTEANDO ESE CASO
        #
        #
   
    # Ahora empieza lo lindo! - Primero hacemos que se puedan sumar enteros con fracciones
    # y fracciones con enteros
    #
    def testISumaDeEnteroYFraccion(self):
   
        self.assertEqual (self.seisQuintos,self.uno+self.unQuinto)
   
    def testJSumaDeFraccionYEntero(self):
   
        self.assertEqual (self.seisQuintos,self.unQuinto+self.uno)
   
    #
    # Hacemos lo mismo para la multipliacion
    #
    def testKMultiplicacionDeEnteroPorFraccion(self):
   
        self.assertEqual(self.dosQuintos,self.dos*self.unQuinto)
   
    def testLMultiplicacionDeFraccionPorEntero(self):
   
        self.assertEqual(self.dosQuintos,self.unQuinto*self.dos)
   
    #
    # Hacemos lo mismo para la division
    #
   
    def testMDivisionDeEnteroPorFraccion(self):
   
        self.assertEqual(self.cincoMedios,self.uno/self.dosQuintos)
   
    def testNDivisionDeFraccionPorEntero(self):
   
        self.assertEqual(self.dosVeinticincoavos,self.dosQuintos/self.cinco)
   
    #
    # Ahora si empezamos con problemas de reduccion de fracciones
    #
   
    def testOUnaFraccionPuedeSerIgualAUnEntero(self):
   
        self.assertEquals(self.dos,self.cuatroMedios)
   
    def testPLasFraccionesAparentesSonIguales(self):
   
        self.assertEquals(self.unMedio,self.dosCuartos)
   
    #
    # Las fracciones se reducen utilizando el maximo comun divisor (mcd)
    # Por lo tanto, para a/b, sea c = mcd (a,b) => a/b reducida es:
    # (a/c) / (b/c).
    #
    # Por ejemplo: a/b = 2/4 entonces c = 2. Por lo tanto 2/4 reducida es:
    # (2/2) / (4/2) = 1/2
    #
    # Para obtener el mcd pueden usar el algoritmo de Euclides que es:
    #
    # mcd (a,b) =
    # si b = 0 --> a
    # si b != 0 -->mcd(b, restoDeDividir(a,b))
    #
   
    # Ejemplo:
    # mcd(2,4) ->
    # mcd(4,restoDeDividir(2,4)) ->
    # mcd(4,2) ->
    # mcd(2,restoDeDividir(4,2)) ->
    # mcd(2,0) ->
    # 2
    #
   
    def testQLaSumaDeFraccionesPuedeDarEntero(self):
   
        self.assertEquals (self.uno,self.unMedio+self.unMedio)
   
    def testRLaMultiplicacionDeFraccionesPuedeDarEntero(self):
   
        self.assertEquals(self.dos,self.cuatro*self.unMedio)
   
    def testSLaDivisionDeEnterosPuedeDarFraccion(self):
   
        self.assertEquals(self.unMedio, self.dos/self.cuatro)
   
    def testTLaDivisionDeFraccionesPuedeDarEntero(self):
   
        self.assertEquals(self.uno, self.unMedio/self.unMedio)
   
    def testUNoSePuedeDividirEnteroPorCero(self):
   
        try:
            self.uno/self.cero
            self.fail()
        except Exception as e:
            self.assertEquals(self.descripcionDeErrorDeNoSePuedeDividirPorCero(),e.message)
   
    def testVNoSePuedeDividirFraccionPorCero(self):
   
        try:
            self.unQuinto/self.cero
            self.fail()
        except Exception as e:
            self.assertEquals(self.descripcionDeErrorDeNoSePuedeDividirPorCero(),e.message)
       
    # Este test puede ser redundante dependiendo de la implementacion realizada
   
    def testWNoSePuedeCrearFraccionConDenominadorCero(self):
   
        try:
            self.crearFraccionCon(self.uno,self.cero)
            self.fail()
        except Exception as e:
            self.assertEquals(self.descripcionDeErrorDeNoSePuedeDividirPorCero(),e.message)
   
    def crearFraccionCon(self, numerador, denominador):
   
        return Fraccion(numerador, denominador)
   
    def descripcionDeErrorDeNoSePuedeDividirPorCero(self):
   
        return Numero.DESCRIPCION_DE_ERROR_DE_DIVISION_POR_CERO


En futuros posts voy a ir factorizando este código para que quede más lindo.

Happy hacking,
Aureliano.