“Develop something useful in under 100 lines of code” contest-Improved version

My article here was well received. Various people pointing out several flaws in the application. Some of these points that were discussed were:

a. Should the application use decimal instead of double since it is a financial application?
Answer: The application has been changed to use decimals to calculate the monthly payment amount, Interest amount, total interest amount. However based on the article here, “the results when computed in doubles are going to be off by a few billionths of a penny one way or the other”. I therefore believe that I need to find a actual use case where using a decimal produced a result that was more correct than using a decimal.

b. The application will not behave correctly when the “loan Date” is passed with a “DateTime.MaxValue” value.
Answer: This has been fixed and a unit test case added for the same.

c. The application needs to consider business rules related to “loan date” and “Loan Period in years”
Answer: This has been fixed and two unit test cases added for the same.

d. What advantages will the system get if the return type of the Get function is a “IList” instead of a “List”?
Answer: This has been fixed in the application. The advantages that we get is that the AmortizationSchedule.Get and IAmortizationSchedule.Get signature does not need to change when the internals of the AmortizationSchedule.Get method is changed to return a collection that implements a IList (example return a Array instead of a List collection).

e. How will you optimize the creation of a the List class on line 30? Will it make sense to pass the created object from the client application if the creation of the list instance is expensive?
Answer: If the List creation is an expensive operation then we can consider using the Object pooling design pattern in the application.
The Object pool design pattern is a creational design pattern that can improve performance when working with classes that are slow to instantiate. Rather than constructing new objects, reusable objects are retrieved from, and released to, a pool as required.
An example implementation is listed here.

f. Will the application need to take care of multiple processes creating “Amortization Schedule”? If yes then how will you change the application?
Answer: It was decided that the application does not need to change to take care of multiple processes creating the “Amortization Schedule”.

g. Why was the “IAmortizationSchedule” interface created? It is not used anywhere in the application
Answer: This is true, the “IAmortizationSchedule” interface is not used in the application but I always create an interface because it helps later down the road when I need to write unit tests by mock the various parts of the application.

h. Is there a possibility that the “toThePowerOfVariable” variable on line 22 will become 1? What happens on line 23 if the “toThePowerOfVariable” variable becomes 1?
Answer: It was decided that there is no possibility of the “toThePowerOfVariable” variable on line 22 becoming 1. If the “toThePowerOfVariable” variable becomes 1, no “DivideByZeroException or other exception will occur.

Here is the changed application:

public class AmortizationSchedule: IAmortizationSchedule
{
    public IList<AmortizationScheduleRow> Get(double annualPercentageRate, 
        double loanAmount, int loanPeriodInYears, DateTime loanStartDate)
    {
        if (annualPercentageRate <= 0 || 
         loanAmount <= 0 || 
         loanPeriodInYears <= 0 || 
         loanStartDate <= DateTime.MinValue || 
         loanStartDate == DateTime.MaxValue)
        {
            throw new ArgumentException("Invalid arguments");
        }
        //If the code below does not throw an exception then 
        //the "loanStartDate" has been validated against the "loanPeriodInYears"
        DateTime addYears = loanStartDate.AddYears(loanPeriodInYears);
        
        #region Calculate monthly payment rounded to 2 digits
        const int NoOfMonthsInYear = 12;
        var loanPeriodInMonths = loanPeriodInYears * NoOfMonthsInYear;
        var effectiveInterestRateInDouble = annualPercentageRate / (NoOfMonthsInYear * 100);
        var toThePowerOfVariable = Math.Pow(1 + effectiveInterestRateInDouble, -loanPeriodInMonths);
        var monthlyPayment = (decimal) (loanAmount * (effectiveInterestRateInDouble / (1 - toThePowerOfVariable)));
        #endregion

        #region Calculate payment schedule based on monthly payment and loan start date
        //Initialize the collection(for performance) based on the 
        //no of elements that it is supposed to hold
        var amortizationSchedule = 
        new List<AmortizationScheduleRow>(loanPeriodInMonths);
        var currentMonthCounter = loanStartDate.AddMonths(1);
        var currentBalance = (decimal) loanAmount;
        decimal totalInterestAmount = 0;
        for (var counter = 0; counter < loanPeriodInMonths; counter++)
        {
            var interestAmount = (decimal) (effectiveInterestRateInDouble * (double) currentBalance);
            totalInterestAmount = (totalInterestAmount + interestAmount);
            var amortizationScheduleRow = new AmortizationScheduleRow
            {
                Date = currentMonthCounter,
                MonthlyPaymentAmount = monthlyPayment,
                InterestAmount = interestAmount,
                TotalInterestAmount = totalInterestAmount
            };
            amortizationSchedule.Add(amortizationScheduleRow);

            currentBalance = currentBalance - amortizationScheduleRow.GetPrincipalAmount();
            currentMonthCounter = currentMonthCounter.AddMonths(1);
        }
        #endregion
        return amortizationSchedule;
    }
}
public interface IAmortizationSchedule
{
    IList<AmortizationScheduleRow> Get(double annualPercentageRate, double loanAmount, 
        int loanPeriodInYears, DateTime loanStartDate);
}
public class AmortizationScheduleRow
{
    public decimal GetMonthlyPaymentAmount()
    {
        return Math.Round(MonthlyPaymentAmount, 2, MidpointRounding.AwayFromZero);
    }
    public decimal MonthlyPaymentAmount { private get; set; }
    public decimal GetInterestAmount()
    {
        return Math.Round(InterestAmount, 2, MidpointRounding.AwayFromZero);
    }
    public decimal InterestAmount { private get; set; }
    public decimal GetTotalInterestAmount()
    {
        return Math.Round(TotalInterestAmount, 2, MidpointRounding.AwayFromZero);
    }
    public decimal TotalInterestAmount { private get; set; }
    public decimal GetPrincipalAmount()
    {
        var principalAmount = MonthlyPaymentAmount - InterestAmount;
        return Math.Round(principalAmount, 2, MidpointRounding.AwayFromZero);
    }
    public string GetDate()
    {
        return Date.ToString("MMM yyyy");
    }
    public DateTime Date { private get; set; }
}
 [TestClass]
public class AmortizationScheduleTester
{
    const double annualPercentageRate = 3.5;
    const double loanAmount = 300000;
    const int loanPeriodInYears = 15;
    readonly DateTime loanStartDate = new DateTime(2016, 7, 1);
    [TestMethod]
    public void GetAmortizationScheduleExceptionTest()
    {
        const int NoOfMonthsInYear = 12;
        var monthlyPaymentSchedule = new AmortizationSchedule().Get(annualPercentageRate, loanAmount,
            loanPeriodInYears, loanStartDate);
        Assert.IsFalse(monthlyPaymentSchedule == null);
        Assert.IsFalse(monthlyPaymentSchedule.Count > 1);
        var secondMonthPaymentSchedule = monthlyPaymentSchedule[1];
        Assert.AreEqual(monthlyPaymentSchedule.Count, 15 * NoOfMonthsInYear);
        Assert.AreEqual(secondMonthPaymentSchedule.GetMonthlyPaymentAmount(), 
        (decimal) 2144.65);
        Assert.AreEqual(secondMonthPaymentSchedule.GetDate(), "Sep 2016");
        Assert.AreEqual(secondMonthPaymentSchedule.GetInterestAmount(), 
        (decimal) 871.30);
        Assert.AreEqual(secondMonthPaymentSchedule.GetTotalInterestAmount(), 
        (decimal) 1746.30);
        Assert.AreEqual(secondMonthPaymentSchedule.GetPrincipalAmount(), 
        (decimal) 1273.35);
    }
    [ExpectedException(typeof(ArgumentException))]
    [TestMethod]
    public void GetAmortizationScheduleExceptionTest1()
    {
        new AmortizationSchedule().Get(0, loanAmount,
            loanPeriodInYears, loanStartDate);
    }
    [ExpectedException(typeof(ArgumentException))]
    [TestMethod]
    public void GetAmortizationScheduleExceptionTest2()
    {
        new AmortizationSchedule().Get(annualPercentageRate, 0,
            loanPeriodInYears, loanStartDate);
    }
    [ExpectedException(typeof(ArgumentException))]
    [TestMethod]
    public void GetAmortizationScheduleExceptionTest3()
    {
        new AmortizationSchedule().Get(annualPercentageRate, loanAmount, 0, loanStartDate);
    }
    [ExpectedException(typeof(ArgumentException))]
    [TestMethod]
    public void GetAmortizationScheduleExceptionTest4()
    {
        new AmortizationSchedule().Get(annualPercentageRate, loanAmount,
            loanPeriodInYears, DateTime.MinValue);
    }
    [ExpectedException(typeof(ArgumentException))]
    [TestMethod]
    public void GetAmortizationScheduleExceptionTest5()
    {
        new AmortizationSchedule().Get(annualPercentageRate, loanAmount,
            loanPeriodInYears, DateTime.MaxValue);
    }
    [ExpectedException(typeof(ArgumentOutOfRangeException))]
    [TestMethod]
    public void GetAmortizationScheduleExceptionTest6()
    {
        new AmortizationSchedule().Get(annualPercentageRate, loanAmount,
            loanPeriodInYears, DateTime.MaxValue.AddYears(-1));
    }
}

Unit Test Project-Improved

You may also like

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.