#!/usr/bin/python
# -*- coding: utf-8 *-*

"""
Trois petits algos de calul sur les prêts à intérêts composés ; connaissant le
capital emprunté, on peut calculer l'un des trois paramètres suivant,
les deux autres étant donnés :

- la prime mensuelle ;
- le temps de remboursement ;
- le taux.

le temps est donné en nombre de mois, les taux sont donnés en pourcentage, le
taux mensuel est le taux annuel divisé par 12 (ce qui est un peu approximatif
mais d'usage répandu).
"""

from math import fabs, log

class LoanError:
    """
Classe des erreurs déclenchées par les méthodes de la classe Loan
    """
    def __init__(self, msg):
        self.msg = msg

    def __str__(self):
        return self.msg

class Loan:
    """
Classe implémentant les méthodes pour les calculs de taux d'intérêt, voir la
description du module loan.py
    """

    def __init__(self, month_rate = None, year_rate = None,
                 capital = None, time = None, prime = None):
        self._rate    = None
        self._capital = None
        self._time    = None
        self._prime   = None

        if month_rate is not None:
            if year_rate is not None:
                raise LoanError("Spécifier seulement un taux mensuel ou annuel")
            self.month_rate(month_rate)
        elif year_rate is not None:
            self.year_rate(year_rate)
        self.capital(capital)
        self.time(time)
        self.prime(prime)

        self.compute_year_rate = self.compute_rate

    def __repr__(self):
        repr = "<Loan instance"
        if self.rate() is not None:
            repr += " year_rate = %2.2f" % self.year_rate()
        if self.capital() is not None:
            repr += " capital = %.2f" %self.capital()
        if self.time() is not None:
            repr += " time = %d" % self.time()
        if self.prime() is not None:
            repr += " prime = %.2f" % self.prime()
        t = self.total()
        if t is not None:
            repr += " total = %.2f" % t

        repr += ">"
        return repr

    def __str__(self):
        str = "Loan:"
        if self.rate() is not None:
            str += "\n\tyear_rate = %.2f" % self.year_rate()
        if self.capital() is not None:
            str += "\n\tcapital   = %.2f" %self.capital()
        if self.time() is not None:
            str += "\n\ttime      = %d" % self.time()
        if self.prime() is not None:
            str += "\n\tprime     = %.2f" % self.prime()
        t = self.total()
        if t is not None:
            str += "\n\ttotal     = %.2f" % t
        return str

    def rate(self, rate = None):
        """
Affectation et consultation du taux d'intérêts du prêt. Il s'agit du taux
mensuel et c'est un nombre compris entre 0 et 1 (plus précisément entre 0 et
0.03, ce dernier étant déjà prohibitif (0.03 correspond à un taux annuel
d'environ 40%).
        """
        if rate is not None:
            try:
                rate = float(rate)
            except ValueError:
                raise LoanError("Le taux doit être un nombre positif "
                                "inférieur à 1")
            if rate <= 0 or rate > 1.0:
                raise LoanError("Taux invalide")
            self._rate = rate

        return self._rate

    def month_rate(self, rate = None):
        """
Affectation et consultation du taux d'intérêt mensuel (en pourcentage).
        """
        if rate is not None:
            try:
                return self.rate(float(rate)/100)*100
            except ValueError:
                raise LoanError("Le taux mensuel doit être "
                                "un pourcentage positif")
        else:
            return self.rate()*100

    def year_rate(self, rate = None):
        """
Affectation et consultation du taux d'intérêt annuel (en pourcentage).
        """
        if rate is not None:
            try:
                return self.rate(float(rate)/1200)*1200
            except ValueError:
                raise LoanError("Le taux annuel doit être un "
                                "pourcentage positif")
        else:
            rate = self.rate()
            if rate:
                return rate*1200
            else:
                return None

    def capital(self, cap = None):
        """
Affectation et consultation du capital emprunté.
        """
        if cap is not None:
            try:
                cap = float(cap)
            except ValueError:
                raise LoanError("Le capital emprunté doit être un "
                                "nombre positif")
            if cap < 0:
                raise LoanError("Capital invalide")
            self._capital = cap
        return self._capital

    def total(self):
        """
Calcul du total remboursé.
        """
        n = self.time()
        p =  self.prime()
        if n is not None and p is not None:
            return n * p
        else:
            return None

    def time(self, time = None):
        """
Affectation et consultation du temps de remboursement (nombre de mois).
        """
        if time is not None:
            try:
                time = int(time)
            except ValueError:
                raise LoanError("Le nombre de mensualilités doit être "
                                "un entier positif")
            if time <= 0:
                raise LoanError("Le nombre de mensualités doit être positif")
            self._time = time
        return self._time

    def prime(self, prime = None):
        """
Affectation et consultation du montant de la prime mensuelle.
        """
        if prime is not None:
            try:
                self._prime = float(prime)
            except ValueError:
                raise LoanError("La prime mensuelle doit être un "
                                "nombre positif")
        return self._prime

    def compute_prime(self):
        """
Calcule la prime de remboursement en appliquant la formule :

        p = c tau^n(tau - 1)/(tau^n - 1)

où c est le capital emprunté, tau le coefficient d'intérêt (c'est à dire
1 + taux mensuel), n est le nombre de mois de remboursement.
        """
        if self.capital() is None:
            raise LoanError("Entrer le capital")
        if self.rate() is None:
            raise LoanError("Entrer un taux")
        if self.time() is None:
            raise LoanError("Entrer le nombre de mensualités")

        t = 1 + self.rate()
        tn = t**self.time()

        return self.prime(tn * (t - 1) * self.capital() / (tn - 1))

    def compute_time(self):
        """
Calcule le temps de remboursement (nombre de mois) en appliquant la formule :

        t = log(p/(p - rho c))/log(rho + 1)

où rho est le taux mensuel et p la prime de remboursement mensuel.
        """

        c = self.capital()
        if c is None:
            raise LoanError("Entrer le capital")

        r = self.rate()
        if r is None:
            raise LoanError("Entrer le taux")

        p = self.prime()
        if p is None:
            raise LoanError("Entrer le montant des mensualités")

        if p < r * c: raise LoanError("La mensualité n'est pas assez élevée")

        self.time(int(log(p/(p - r * c)) / log(1 + r)) + 1)

        return self.time()

    def compute_time_iter(self):
        """
Calcule le temps de remboursement (nombre de mois) en itérant la formule :

        c_{n+1} = c_n + rho c_n - p

où c_n est le capital dû au n-ième mois, rho est le taux mensuel et p la
prime de remboursement mensuel. Rend le premier n tel que c_n < 0.
        """
        c = self.capital()
        if c is None:
            raise LoanError("Entrer le capital")

        r = self.rate()
        if r is None:
            raise LoanError("Entrer le taux")

        p = self.prime()
        if p is None:
            raise LoanError("Entrer le montant des mensualités")

        if p < r * c:
            raise LoanError("Les mensualités ne sont pas assez élevées")

        t = 0
        while c > 0:
            c = c - p + r * c
            t = t + 1

        return t


    def compute_rate(self):
        """
Calcule le taux d'intérêt en résolvant par méthode dichotomique l'équation :

        x**n - p/c * (x**(n-1) + ... + 1) = 0

où c est le capital emprunté, n le nombre de mensualités et p le montant
du remboursement mensuel
        """

        c = self.capital()
        if c is None:
            raise LoanError("Entrer le capital")

        p = self.prime()
        if p is None:
            raise LoanError("Entrer le montant des mensualités")

        n = self.time()
        if n is None:
            raise LoanError("Entrer le nombre de mensualités")

        a = p/c

        def poly(r):
            if r == 1.0:
                return 1.0 - n * a
            else:
                rn = r**n
                return rn - a * (rn - 1) / (r - 1)

        x = 1.0
        y = 2.0
        px = poly(x)
        py = poly(y)

        if px >= 0.0 or py <= 0.0:
            raise LoanError("Impossible de calculer le taux, "
                            "peut-être que la prime mensuelle est "
                            "insuffisamment élevée")

        while y - x > 0.0000001:
            z = (x + y)/2
            pz = poly(z)
            if pz == 0.0:
                break
            elif pz < 0.0:
                x = z
                px = pz
            else:
                y = z
                py = pz

        self.rate(z - 1)
        return self.year_rate()

    def plan(self):
        """
Retourne le plan de remboursement
        """
        capital = self.capital()
        if not capital:
            raise LoanError("Entrer le capital emprunté")

        prime   = self.prime()
        if not prime:
            prime = self.compute_prime()

        time    = self.time()
        if not time:
            time = self.compute_time()


        rate    = self.rate()
        if not rate:
            self.compute_rate()
            rate = self.rate()

        month   = 0
        total_refunded = 0
        total_interest = 0
        plan    = [(month, capital, 0, total_refunded, total_interest)]
        while month < time:
            month += 1
            interest = capital * rate
            total_refunded += prime
            total_interest += interest
            capital  = capital + interest - prime
            plan.append((month, capital, interest, total_refunded,
                         total_interest))

        return plan

    def export(self, filename, plan = None):
        if plan is None:
            plan = self.plan()

        try:
            csv = file(filename, "w")
        except IOError, err:
            raise LoanError(str(err))

        csv.write("\"Mois\",\"Capital\",\"Intérêt\",")
        csv.write("\"Total remboursé\",\"dont intérêts\",")
        csv.write("\"Taux annuel:\",\"%f\"," % self.year_rate())
        csv.write("\"Mensualité:\",\"%f\"\n" % self.prime())

        csv_format_line = "\"%2d\",\"%f\",\"%f\",\"%f\",\"%f\"\n"

        for (month, capital, interest, total_refunded, total_interest) in plan:
            csv.write(csv_format_line % (month, capital, interest,
                                         total_refunded, total_interest))
        csv.close()