Skip to Content
Course content

154: Precision Arithmetic for Financial Calculations

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've spent any time in professional software engineering, you've probably heard the horror stories about floating-point errors. In a hobby project, a rounding error at the eighth decimal place is a curiosity. In a financial system, it's a bug that gets you fired or, worse, audited by the government.

Today, we're going to build a small utility to calculate a "Loyalty Discount" for an e-commerce store. The rule is simple: we take a subtotal, apply a percentage discount, and then add a flat-rate shipping fee. It sounds trivial, but the way we handle the numbers makes all the difference.

The temptation of the Double

When I first started writing this, my instinct was to just use Double. It's fast, it's built-in, and for most things, it's "close enough." Let's look at what happens when we try to calculate a 10% discount on a $19.99 item.

val subtotal = 19.99
val discountRate = 0.10
val discountAmount = subtotal * discountRate
println(s"Discount: $discountAmount") 
// Output: Discount: 1.9990000000000001

There it is. That tiny ...0001 at the end. It happens because Double uses binary floating-point representation, which cannot exactly represent many decimal fractions. If you sum this up over a million transactions, your books will never balance. We need BigDecimal.

Switching to BigDecimal for Exactness

Scala's BigDecimal is a wrapper around java.math.BigDecimal. It allows us to specify the exact precision we need. I'll rewrite our logic to ensure we aren't losing pennies to the void.

val subtotal = BigDecimal("19.99")
val discountRate = BigDecimal("0.10")
val discountAmount = subtotal * discountRate
val finalPrice = subtotal - discountAmount

println(s"Final Price: $finalPrice") 
// Output: Final Price: 17.991

That's better, but we have a new problem: 17.991 isn't a valid currency amount. We can't charge a customer a tenth of a cent. We need to round this to two decimal places.

The "Non-Terminating Decimal" Trap

Here is where I usually trip up when I'm rushing. I tried to divide the total by a factor to split the cost between two promotional accounts, and I did this:

val splitCost = finalPrice / BigDecimal("3")
// Result: java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

I forgot that BigDecimal is too precise. When you divide 17.991 by 3, you get a repeating decimal. Since BigDecimal tries to be exact by default, it throws an exception because it can't represent an infinite string of numbers. To fix this, we have to tell Scala how to round the result using a MathContext or the setScale method.

Polishing the Calculation Logic

Now, let's put this all together into a clean, reusable function. I'll use RoundingMode.HALF_UP, which is the standard "round half up" logic we all learned in school and that most accounting software expects.

import scala.math.BigDecimal.RoundingMode

def calculateFinalTotal(subtotal: BigDecimal, discountPercent: BigDecimal): BigDecimal = {
  val discountAmount = subtotal * (discountPercent / 100)
  val total = subtotal - discountAmount
  
  // We set the scale to 2 decimal places and use HALF_UP rounding
  total.setScale(2, RoundingMode.HALF_UP)
}

val myTotal = calculateFinalTotal(BigDecimal("19.99"), BigDecimal("10.0"))
println(s"The customer pays: $$${myTotal}") 
// Output: The customer pays: $17.99

By explicitly defining the scale and the rounding mode, we've moved from "approximate math" to "financial math." We are now in control of every single digit.




📋 Practical Task

Build a Compound Interest Calculator for a Savings Account

Your task is to create a small program that calculates the future value of a savings account. You must avoid Double entirely to ensure financial precision.

  • Create a function calculateCompoundInterest(principal: BigDecimal, annualRate: BigDecimal, years: Int).
  • The formula for annual compounding is: Future Value = Principal * (1 + Rate)^Years.
  • The annualRate will be passed as a percentage (e.g., 5.0 for 5%).
  • The final result must be rounded to exactly 2 decimal places using RoundingMode.HALF_UP.
  • Test your function with a principal of 1000.00, a rate of 3.5, and a duration of 10 years.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.