Calculating Credit Card Payoff in C# (2023 C# Advent)

6 minute read article dotnet   microsoft   advent   spectre.console   csharp Comments

Note: This post is part of the 2023 C# Advent! Check out the rest of the entries here.

The holiday season, at least in the United States, is a time when many people spend more money than they have.

The average credit card debt in 2023 is $6,365 per cardholder, according to Experian. This is an increase from $5,010 in 2022. The average credit card interest rate is 22.16% in May 2023, according to the Federal Reserve.

Average American Credit Card Debt in 2023

Creating and sticking to a budget can be difficult, so many people find themselves using credit cards to make their holiday purchases. In order to get 2024 started on the right foot, it’s a good idea to have a plan for getting those cards paid off as quickly as possible.

This year, I’m reaching back into Brian P. Hogan’s excellent Exercises for Programmers book for a fun exercise to help you pay off your credit cards.

The Problem

The basic problem Brian lays out is this:

Write a program that will help you determine how many months it will take to pay off a credit card balance. The program should ask the user to enter the balance of a credit card and the APR of the card. The program should then return the number of months needed.

He didn’t explicitly say that the program also needs to ask for the monthly payment, so I’ll handle that, too.

Setup

I’ll be using .NET 6 and C# 10 for this project, and I’ll also be using Spectre.Console again to help with both the input and output of the program. All screenshots will be from my Mac, but the code should work on Windows and Linux as well. Oh, and one final note - my IDE of choice lately is JetBrains Rider!

The Formula

The formula, as described in Brian’s book is:

The Formula

  • n is the number of months
  • i is the daily rate (APR divided by 365)
  • b is the balance
  • p is the monthly payment

Due to the data involved, it’s possible to introduce a lot of subtle bugs, so be careful!

The Code

Get started by creating a new console application and adding Spectre.Console.


dotnet new console -o CreditCardPayoff.Main
cd CreditCardPayoff.Main
dotnet add package Spectre.Console

The first step is to replace the default “Hello, World” with “Credit Card Payoff Calculator.”


Console.WriteLine("Credit Card Payoff Calculator");

Before diving into the formula, there’s a lot of low-hanging fruit to take care of like prompting the user for the APR and balance. Spectre.Console makes this so simple, it’s almost embarrassing:


var annualPercentageRate = AnsiConsole.Prompt(
    new TextPrompt<double>("What is the APR on the card (as a percent)?")
        .PromptStyle("green")
        .ValidationErrorMessage("[red]That's not a valid rate[/]")
        .Validate(apr =>
            apr switch
            {
                < 0 => ValidationResult.Error("[red]The APR must be great than or equal to zero.[/]"),
                > 100 => ValidationResult.Error("[red]The APR cannot be greater than 100.[/]"),
                _ => ValidationResult.Success(),
            })
);

I wasn’t sure what the upper limit of the APR entry should be, so I went with 100%. Feel free to adjust this to your liking. Prompting for the balance and monthly payment is just as easy, and I also added a few lines to display the values entered:


var cardBalance= AnsiConsole.Prompt(
    new TextPrompt<decimal>("What is the balance on the card?")
        .PromptStyle("green")
        .ValidationErrorMessage("[red]That's not a valid balance[/]")
        .Validate(balance =>
            balance switch
            {
                <= 0 => ValidationResult.Error("[red]The balance must be greater than zero.[/]"),
                _ => ValidationResult.Success(),
            })
);

var monthlyPayment= AnsiConsole.Prompt(
    new TextPrompt<double>("How much do you pay each month?")
        .PromptStyle("green")
        .ValidationErrorMessage("[red]That's not a valid amount.[/]")
        .Validate(payment =>
            payment switch
            {
                <= 0 => ValidationResult.Error("[red]The amount must be greater than zero.[/]"),
                _ => ValidationResult.Success(),
            })
);

Console.WriteLine($"The balance is {cardBalance}");
Console.WriteLine($"The APR is {annualPercentageRate}");
Console.WriteLine($"The payment is {monthlyPayment}");

When I first looked at the formula, I had no idea how I was going to implement it in C#, so I started with the simplest thing I could - the daily rate. This is simply the APR divided by 365.


var dailyRate = annualPercentageRate / 365 / 100;

The extra division by 100 is to convert the percentage to a decimal.

Now that we have all the data necessary to calculate the number of months, it’s time to implement the formula. Remember, the formula from the book looks like this:

The Formula

In order to wrap my non-Math head around it, I flattened it out a bit:


n = -(1/30) * log(1 + b/p * (1 - (1 + i)^30)) / log(1 + i)

The Math class will come in handy here with the Log and Pow methods.


var months = -(1.0 / 30.0) 
        * Math.Log(1 + cardBalance / monthlyPayment * (1 - Math.Pow(1 + dailyRate, 30))) 
        / Math.Log(1 + dailyRate);
Console.WriteLine($"The number of months is {Math.Ceiling(months)}");

I’m using Math.Ceiling for the result to round up to the nearest whole number since we can’t really have a fraction of a month, at least not in this context.

The first piece of the formual represents the inverse of the number of days in a month. Next, I use Math.Log to help calculate the ratio of the balance to the payment, adjusted for the daily interest rate over a 30-day period.

After all that, here’s what the output should look like

The Output

Since everything is currently in the Program class, I’m going to refactor a bit, moving the calculation into another class, simplifying the Program class to just the Spectre.Console prompts and a call to our refactored function.

The first refactor is to move the calculation into its own method and do a little cleanup like moving the call to Math.Ceiling to the return statement instead of having the caller do it. I also removed some of the console output.


var months = GetMonthsUntilPayoff(cardBalance, monthlyPayment, annualPercentageRate);
Console.WriteLine($"It will take you {months} months to pay off your balance of {cardBalance:C} at {annualPercentageRate}% APR with a monthly payment of {monthlyPayment:C}.}");    

double GetMonthsUntilPayoff(double balance, double payment, double apr)
{
    var dailyRate = apr / 365 / 100;
    return Math.Ceiling(
      -(1.0 / 30.0) 
      * Math.Log(1 + balance / payment * (1 - Math.Pow(1 + dailyRate, 30))) 
      / Math.Log(1 + dailyRate));
}

I created a new class in the Main project and named it CreditCardPayoffCalculator and moved the GetMonthsUntilPayoff method into it and made the method public. As part of that refactor, I modified the Program.cs to instantiate the new class and call the method.


var calc = new CreditCardPayoffCalculator();
var months = calc.GetMonthsUntilPayoff(cardBalance, monthlyPayment, annualPercentageRate);

Running the program now looks like this:

The Final Output

At this point, everything is working, at least for the happy path. In my testing, I noticed that if the payment was too small, the result would be NaN. I added a check for this and display a message if the payment is too small.


if (double.IsNaN(months))
{
    Console.WriteLine("The payment is too small.");
}
else
{
    Console.WriteLine($"The number of months is {(months)}");
}

It’s interesting, and probably a good thing, that the formula really doesn’t work for payments that are too small. I’m sure there’s a way to make it work, but I’m not going to worry about it for now.

This was a fun exercise, but not all that complex. With more time, I’d probably tackle the challenge Brian describes in the book:

Rework the formula so the program can accept the number of months as an input and compute the monthly payment. Create a version of the program that lets the user choose whether to figure out the number of months until payoff or the amount needed to pay per month.

For now though, I’m happy with it. I hope you enjoyed this one as much as I did.

Make sure you check out the code on GitHub!


A seal indicating this page was written by a human

Updated:

Comments