How to handle money in software (Kotlin)

How to handle money in software (Kotlin)

Main role of majority of software is to generate or save money. Good portion of this software also directly work with money. Bad implementations can cause rounding errors which can lead to losing money or worse.

There are a lot of things that is to be considered when we want to correctly implement Money and we'll briefly introduce some of them.

Best idea seems to be to create Money class, but the question is how should it look like, what should its responsibilities be, or what data should it carry.

Basic design

Our money structure should contain at least two properties to represent a meaningful money data: amount and currency. Another requirement is for it to be represented by a Value Object.

Currency's type can be represented by an enumeration, which holds all currencies that are valid to our system. Better solution would be to use Currency class from the Java util library.

Now we have to choose what is the most suitable data structure for amount. Maybe float or double will come to mind, but this would be a terrible idea due to floating point arithmetic. You can try this code:

fun main() {
    var res = 0.0f
    
    for (i in 0 until 10000) {
        res += 0.1f
    }
    
    println(res)
}

The choice lies between fixed-point decimals and integers which are represented by BigDecimal, BigInteger, long. The decimals have a few drawbacks. For example this results to false:

import java.math.BigDecimal

fun main() {
    println(BigDecimal("0.0").equals(BigDecimal("0.00")))
}

The recommended way is to use integral representation and store the amount of cents. The only way we have to figure out now is to be able to print string value

So our class can currently look like this:

import java.math.BigDecimal
import java.util.Currency

data class Money private constructor(
	private val cents: Long,
    val currency: Currency) {
    
    val amount: BigDecimal
        get() {
            return BigDecimal.valueOf(cents, currency.defaultFractionDigits)
        }
    
    companion object {
        private val cents = intArrayOf(1, 10, 100, 1000)
        
        fun getInstance(currency: Currency, amount: Long): Money {
            return Money(currency, amount*centMultiplierOf(currency))
        }
        
        fun getInstance(currency: Currency, amount: Double): Money {
            val cents = amount*centMultiplierOf(currency)
            return Money(currency, cents.toLong())
        }
        
        fun EUR(amount: Long): Money {
            val currency = Currency.getInstance("EUR")
            return Money(currency, amount*centMultiplierOf(currency))
        }
        
        private fun centMultiplierOf(currency: Currency) =
        	cents[currency.defaultFractionDigits]
    }
    
    override fun toString(): String {
        return "${amount.toString()} ${currency.symbol}"
    }
}
    

Note that its solves few possible edge-cases:

  • it encapsulates the way it works with whole cents instead of fractions and does not expose it to the API
  • there are named constructors in favor of classic constructors to provide better extensibility
  • there are multiple named constructors to provide creation from different data types
  • there is a static constructor to construct particular currency (these can be provided to simplify construction)
  • getter is provided for cents only as mapped value to the BigDecimal representation of amount
  • overriden toString method that displays money in concise and readable way

This data structure is pretty nice, but it does not encapsulate any logic on how to work with it.

Encapsulate the behavior

Let's specify what our Money class should be able to do:

  • addition, subtraction and multiplication
  • comparisons between objects
  • allocation for given ratio array

Before we implement our addition, let's consider that we try to sum up two object with different currencies. What is the best way to handle that ? We should not probably introduce the currency conversion to Money object and leave that responsibility for other parts of code. We can simply consider object with different currency as an invalid argument. Then it's an easy matter.

import java.math.BigDecimal
import java.util.Currency

data class Money private constructor(
	...
    
    fun add(other: Money): Money {
		assertSameCurrencyWith(other)
        return Money(currency, cents + other.cents)
    }
    
    fun subtract(other: Money): Money {
        assertSameCurrencyWith(other)
        return Money(currency, cents - other.cents)
    }
    
    fun multiply(amount: Double): Money = 
    	multiply(BigDecimal(amount))
    
    fun multiply(amount: BigDecimal): Money =
    	getInstance(currency, this.amount.multiply(amount))
    
    private fun assertSameCurrencyWith(other: Money) {
        if (!other.currency.equals(this.currency)) {
            throw IllegalArgumentException()
        }
    }
}

These methods seem enough to sum up, subtract, or divide money, but there is one drawback in case of multiply:

Try this code out:

fun main() {
    println(Money.EUR(0.05).multiply(0.5).multiply(2.0))
}

Since 0.5*2 == 1 is one, in ideal case this should return the same value. But it's not the case, because of rounding limitation, we lose one cent and the result will be 0.04 .

But there is a way to divide money in such way that we don't lose cents.

Allocate method

Matt Foemmel's Conundrum refers to a logical rounding error that occurs with monetary calculations on computer systems that use floating-point numbers to store currency values.

Let's imagine that there is a 0.05$ capital and we want to split it into two accounts (Alice and Bob) in ratio 70 to 30.

How should we do it ? It seems there are only these options:

  • Divide it into fractional cents and split it to 0.035$ and 0.015$ - this results into fractional cents which is in many cases not suitable or valid amount of money
  • Rounded multiplication - rounding modifies result to be 0.04$ and 0.02$ which results into one cent more than previous amount
  • Ceiled multiplication - results in same problem as rounded multiplication
  • Floored multiplication - results in 0.03$ and 0.01$ which means we lose one cent.

It seems none of these options are usable. But there is a solution which results into two amounts that sums up to same sum they were divided from.

The idea is that we calculate the integer value of ratio percentage and assign it to result. So it would add 0.03$ and 0.01$ to Alice and to Bob, respectively. But there is a remainder of calculation of 0.01$.

There are many ways to distribute this remainder relatively fairly. We can assign it sequentially and give remaining cents from first account onwards, since there is no remainder left. We can also assign it randomly, or to accounts with greatest values.

This implementation is using the sequential assign of remainder.

    fun allocate(ratios: List<Long>): List<Money> {
        val total = BigDecimal.valueOf(
            ratios.reduce { ratio, acc -> ratio + acc })
        var remainder = cents
        val resultCents = LongArray(ratios.size)
        
        for (i in ratios.indices) {
            val ratio = ratios[i]
            val cents = BigDecimal.valueOf(cents)
                .multiply(BigDecimal.valueOf(ratio))
                .divide(total)
                .toLong()
            remainder -= cents
            resultCents[i] += cents
        }
        for (i in 0 until remainder.toInt()) {
            resultCents[i] += 1L
        }
        
        return resultCents.map { Money(currency, it) }.toList()
    }

This code will give Alice 0.04$ and Bob 0.01$, which sums up to the original amount.

Cherry on top

We may add more methods to the value object, so we can work with it more intuitively and produce more readable code.

For example it is wise for our value object to implement Comparable interface. Since we are using data class keyword, equals and hashCode are generated for us.

data class Money private constructor(
    private val currency: Currency, 
    private val cents: Long): Comparable<Money> {
	...
    
    override fun compareTo(other: Money): Int {
        assertSameCurrencyWith(other)
        return compareValuesBy(this, other, { it.cents })
    }
}

Now it's relatively easy to add comparison wrappers if we'd need them:

data class Money private constructor(
    private val currency: Currency, 
    private val cents: Long): Comparable<Money> {
   	...
    
    fun greaterThan(other: Money) = compareTo(other) > 0
}

Conclusion

I think it's fair to say that after reading this article you'll be at least able to laugh at this:

and maybe someday use ideas from this article to tackle monetary calculations in your software - without floats.

Resources: