'''\
An experiment with python properties using TVM equations as an example

A TVM object works like a financial calculator.
Given any four of (n, i, pmt, pv, fv) TVM object can calculate the fifth.
This version assumes payments are at end of compounding period.

Example:
>>> loan=TVM()
>>> loan.n=3*12           # set number of payments to 36
>>> loan.pv=6000          # set present value to 6000
>>> loan.i=6/12.          # set interest rate to 6% annual
>>> loan.fv=0             # set future value to zero
>>> loan.pmt              # ask for payment amount
-182.5316247083343        # payment (note sign)

Alternative style:
>>> TVM(n=36, pv=6000, pmt=-200, fv=0).i*12
12.2489388032796
'''

from math import log


class TVM (object):
    '''A Time Value of Money class using properties'''

    def __init__(self, n=None, i=None, pv=None, pmt=None, fv=None):
        self.__n=n
        self.__i=i
        self.__pv=pv
        self.__pmt=pmt
        self.__fv=fv

    def __repr__(self):
        return "TVM(n=%s, i=%s, pv=%s, pmt=%s, fv=%s)" %   \
              (self.__n, self.__i, self.__pv, self.__pmt, self.__fv)

    def __get_a(self):
        '''calculate 'a' intermediate value'''
        return (1+self.__i/100)**self.__n-1

    def __get_b(self):
        '''calculate 'b' intermediate value'''
        return 100/self.__i

    def __get_c(self):
        '''calculate 'c' intermediate value'''
        return self.__get_b()*self.__pmt

    def get_n(self):
        c=self.__get_c()
        self.__n = log((c-self.__fv)/(c+self.__pv))/log(1+self.__i/100)
        return self.__n
    def set_n(self,value):
        self.__n=value
    n = property(get_n, set_n, None, 'number of payments')

    def get_i(self):
        # need to do an iterative solution - no closed form solution exists
        # trying Newton's method
        INTTOL=0.0000001      # tolerance
        ITERLIMIT=1000        # iteration limit
        # initial guess for interest
        if self.__i:
            i0=self.__i
        else:
            i0=1.0
        # 10% higher interest rate - to get a slope calculation
        i1=1.1*i0
        def f(tvm,i):
            '''function used in Newton's method; pmt(i)-pmt'''
            a = (1+i/100)**self.__n-1
            b = 100/i
            out = -(tvm.__fv+tvm.__pv*(a+1))/(a*b)-tvm.__pmt
            return out
        fi0 = f(self,i0)
        if abs(fi0)<INTTOL:
            self.__i=i0
            return i0
        else:
            n=0
            while 1:                    # Newton's method loop here
                fi1 = f(self,i1)
                if abs(fi1)<INTTOL:
                    break
                if n>ITERLIMIT:
                    print("Failed to converge; exceeded iteration limit")
                    break
                slope=(fi1-fi0)/(i1-i0)
                i2=i0-fi0/slope           # New 'i1'
                fi0 = fi1
                i0=i1
                i1=i2
                n+=1
            self.__i = i1
            return self.__i

    def set_i(self,value):
        self.__i=value
    i = property(get_i, set_i, None, 'interest rate')

    def get_pv(self):
        a=self.__get_a()
        c=self.__get_c()
        self.__pv = -(self.__fv+a*c)/(a+1)
        return self.__pv
    def set_pv(self,value):
        self.__pv=value
    pv = property(get_pv, set_pv, None, 'present value')

    def get_pmt(self):
        a=self.__get_a()
        b=self.__get_b()
        self.__pmt = -(self.__fv+self.__pv*(a+1))/(a*b)
        return self.__pmt
    def set_pmt(self,value):
        self.__pmt=value
    pmt = property(get_pmt, set_pmt, None, 'payment')

    def get_fv(self):
        a=self.__get_a()
        c=self.__get_c()
        self.__fv = -(self.__pv+a*(self.__pv+c))
        return self.__fv
    def set_fv(self,value):
        self.__fv=value
    fv = property(get_fv, set_fv, None, 'future value')
