# Defines a class Savings and a class Loans that both derive from a class
# LoanOrSavings, allowing one to set the values of at most N-1 parameters
# from a list of N parameters, and in case the values of N-1 parameters
# are set, computing the value of the missing one.
#
# Written by Eric Martin for COMP9021

from math import log10


INTEREST = 'Interest'
REFERENCE_PERIOD = 'Reference period'
TERM_AMOUNT = 'Term amount'
DURATION = 'Duration'
INITIAL_SUM = 'Initial sum'
FINAL_SUM = 'Final sum'


class LoanOrSavings:
    # The interest has to be provided.
    # reference period, term_amount and duration can be either provided
    # or computed.
    # These parameters need to be complemented with:
    # - final_sum for savings (either provided or computed), useless for
    # loans since it has to be 0 (loan fully repaid)
    # - initial_sum for loans (either provided or computed), useless for
    # savings since it is the same as term_amount.
    def __init__(self, of_Savings_type, *, interest,
                                           reference_period,
                                           term_amount,
                                           duration):
        self._of_savings_type = of_Savings_type
        self._reference_periods = {'year' : 1,
                                   'semester' : 2,
                                   'quarter' : 4,
                                   'month' : 12} 
        self._parameters = {}
        # We will remove INITIAL_SUM from _unknowns when dealing with an object
        # of class Savings, and remove FINAL_SUM from _unknowns when dealing
        # with an object of class Loan.
        self._unknowns = {INITIAL_SUM, TERM_AMOUNT, FINAL_SUM, DURATION}
        self.set_reference_period(reference_period)
        self.set_interest(interest)
        self.set_term_amount(term_amount)
        self.set_duration(duration)
                
    def set_term_amount(self, term_amount):
        # A sum added to the account.
        if self._of_savings_type:
            sign = 1
        # A sum taken from the account.
        else:
            sign = -1
        self._check_and_set_parameter(term_amount, TERM_AMOUNT,
                                      [int, float], sign)

    def set_reference_period(self, reference_period):
        if reference_period not in self._reference_periods:
            raise ValueError('Reference period should be one of {:}'.
                                         format(list(self._reference_periods)))
        self._parameters[REFERENCE_PERIOD] =\
                                      self._reference_periods[reference_period]
        self.reference_period = reference_period

    def set_interest(self, interest):
        self._check_type(interest, INTEREST, [int, float])
        if interest == 0:
            raise ValueError('Interest should not be 0')          
        if interest < 0:
            raise ValueError('Interest should not be negative')          
        self._parameters[INTEREST] = interest
        self.effective_interest = (1 + self._parameters[INTEREST] /
                                         self._parameters[REFERENCE_PERIOD])\
                                      ** self._parameters[REFERENCE_PERIOD] - 1

    def set_duration(self, duration):
        self._check_and_set_parameter(duration, DURATION, [int], 1)

    def _check_and_set_parameter(self, parameter, parameter_name,
                                 valid_types, sign):
        self._check_type(parameter, parameter_name, valid_types)
        self._set_parameter(parameter, parameter_name, sign)
        
    def _check_type(self, parameter, parameter_name, valid_types):
        if all(not isinstance(parameter, valid_type)
                                                for valid_type in valid_types):
            raise TypeError('{:} should be of type in {:}'.
                                          format(parameter_name, valid_types))

    def _set_parameter(self, parameter, parameter_name, sign):
        if parameter * sign < 0:
            raise ValueError('{:} should be of opposite sign'.
                                                       format(parameter_name))
        self._parameters[parameter_name] = parameter
        if parameter:
            if parameter_name in self._unknowns:
                if len(self._unknowns) == 1:
                    raise NoUnknownException(
                                      '{:} is the only unknown parameter'.
                                                       format(parameter_name))
                self._unknowns.remove(parameter_name)
        else:
            self._unknowns.add(parameter_name)
                                                        
    def update(self):
        # solve() is specific to and defined in the subclasses Savings and Loan.
        all_known = self.solve()
        print('Annual interest:\t {:.2f}%'.
                               format(float(self._parameters[INTEREST] * 100)))
        print('Reference period:\t', self.reference_period)
        if not self._of_savings_type:
            if all_known or INITIAL_SUM not in self._unknowns:
                print('Sum borrowed:\t\t {:.2f}'.
                                  format(float(self._parameters[INITIAL_SUM])))
            else:
                print('Sum borrowed:\t\t Unknown')
        if all_known or TERM_AMOUNT not in self._unknowns:
            if self._of_savings_type:
                print('Yearly deposits:\t {:.2f}'.
                                  format(float(self._parameters[TERM_AMOUNT])))
            else:
                print('Monthly repayments:\t {:.2f}'.
                                  format(float(self._parameters[TERM_AMOUNT])))
        else:
            if self._of_savings_type:
                print('Yearly deposits:\t Unknown')
            else:
                print('Monthly repayments:\t Unknown')
        if self._of_savings_type:
            if all_known or FINAL_SUM not in self._unknowns:       
                print('Available sum:\t\t {:.2f}'.
                                    format(float(self._parameters[FINAL_SUM])))
            else:
                print('Available sum:\t\t Unknown')
        if all_known or DURATION not in self._unknowns:
            print('Duration (in years):\t', round(self._parameters[DURATION]))
        else:
            print('Duration (in years):\t Unknown')


class Savings(LoanOrSavings):
    # term_amount is a yearly deposit
    def __init__(self, *, interest,
                          reference_period = 'year',
                          term_amount = 0,
                          duration = 0,
                          final_sum = 0):
        super().__init__(True, interest = interest,
                               reference_period = reference_period,
                               term_amount = term_amount,
                               duration = duration)
        self._unknowns.remove(INITIAL_SUM)
        self.set_final_sum(final_sum)

    def set_final_sum(self, final_sum):
        self._check_and_set_parameter(final_sum, FINAL_SUM, [int, float], 1)

    def solve(self):
        if len(self._unknowns) != 1:
            return False
        if FINAL_SUM in self._unknowns:
            self._parameters[FINAL_SUM] =\
                   self._parameters[TERM_AMOUNT] / self.effective_interest * \
                          ((1 + self.effective_interest) **
                             (self._parameters[DURATION] + 1) -
                                                (1 + self.effective_interest))
        elif TERM_AMOUNT in self._unknowns:
            self._parameters[TERM_AMOUNT] =\
                            self._parameters[FINAL_SUM] *\
                            self.effective_interest / \
                                   ((1 + self.effective_interest) **\
                                         (self._parameters[DURATION] + 1) -
                                                (1 + self.effective_interest))
        else:
            self._parameters[DURATION] =\
                    log10(self._parameters[FINAL_SUM] *\
                            self.effective_interest /\
                                 self._parameters[TERM_AMOUNT] + \
                                    + (1 + self.effective_interest)) / \
                                        log10(1 + self.effective_interest) - 1
        return True


class Loan(LoanOrSavings):
    # term_amount is a monthly repayment
    def __init__(self, *, interest,
                 reference_period = 'year',
                 term_amount = 0,
                 duration = 0,
                 initial_sum = 0):
        super().__init__(False, interest = interest,
                                reference_period = reference_period,
                                term_amount = term_amount,
                                duration = duration)
        self._unknowns.remove(FINAL_SUM)
        self.set_initial_sum(initial_sum)

    def set_initial_sum(self, initial_sum):
        self._check_and_set_parameter(initial_sum, INITIAL_SUM,
                                      [int, float], 1)

    def solve(self):
        if len(self._unknowns) != 1:
            return False
        monthly_interest = (1 + self.effective_interest) ** (1 / 12) - 1
        if  INITIAL_SUM in self._unknowns:
            self._parameters[INITIAL_SUM] =\
                    -self._parameters[TERM_AMOUNT] * ((1 + monthly_interest) **
                            (12 * self._parameters[DURATION]) - 1) /\
                                 monthly_interest / (1 + monthly_interest) **\
                                              (12 * self._parameters[DURATION])
        elif TERM_AMOUNT in self._unknowns:
            self._parameters[TERM_AMOUNT] =\
                    -self._parameters[INITIAL_SUM] * (1 + monthly_interest) **\
                             (12 * self._parameters[DURATION]) * \
                                monthly_interest / ((1 + monthly_interest) **\
                                         (12 * self._parameters[DURATION]) - 1)
        else:
            self._parameters[DURATION] =\
                    log10(self._parameters[TERM_AMOUNT] / \
                        (monthly_interest * self._parameters[INITIAL_SUM] +
                                          self._parameters[TERM_AMOUNT])) / \
                                             (12 * log10(1 + monthly_interest))
        return True

        
class NoUnknownException(Exception):
    pass


if __name__ == '__main__':
    print('TESTING SAVINGS\n')
    savings = Savings(term_amount = 1000,
                      interest = 0.08,
                      duration = 25)
    savings.update()
    print()
    savings.set_term_amount(0)
    savings.update()
    print()
    savings.set_final_sum(78954.42)
    savings.update()
    print()
    savings.set_duration(0)
    savings.update()
    print()
    savings.set_term_amount(1000.00)
    savings.update()
    print('\n\nTESTING LOANS\n')
    loan = Loan(initial_sum = 100000,
                interest = 0.08,
                duration = 20,
                reference_period = 'month')
    loan.update()
    print()
    loan.set_initial_sum(0)
    loan.update()
    print()
    loan.set_term_amount(-836.44)
    loan.update()
    print()
    loan.set_duration(0)
    loan.update()
    print()
    loan.set_initial_sum(100000.0)
    loan.update()
    
        

Resource created Wednesday 09 September 2015, 12:14:44 PM.

file: loans_and_savings.py


Back to top

COMP9021 15s2 (Principles of Programming) is powered by WebCMS3
CRICOS Provider No. 00098G