Developer Playground
When developing financial applications or systems requiring precise calculations, floating-point precision issues can lead to critical bugs. These problems occur in JVM-based languages like Kotlin, and deciding how to store and process values, especially when interacting with databases, is a crucial design decision.
fun main() {
val a = 0.1
val b = 0.2
val sum = a + b
println("0.1 + 0.2 = $sum")
println("Is sum equal to 0.3? ${sum == 0.3}")
}
The result of the above code is:
0.1 + 0.2 = 0.30000000000000004
Is sum equal to 0.3? false
The fact that 0.1 plus 0.2 doesn't equal exactly 0.3 is due to the limitations of binary floating-point representation. These issues can cause serious errors in financial applications.
The first approach is to multiply floating-point values by a specific factor (typically a power of 10) to convert them to integers before storing in the database. This method is particularly effective when dealing with values that have a fixed number of decimal places, such as currency.
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Id
// Entity to store money information
@Entity
class Money(
@Id
val id: Long,
// Store amount as integer (100x the original value)
@Column(name = "amount_in_cents")
val amountInCents: Long
) {
// Calculate actual amount (integer → decimal)
val amount: Double
get() = amountInCents / 100.0
companion object {
// Convert decimal value to storage integer
fun fromAmount(id: Long, amount: Double): Money {
val amountInCents = (amount * 100).toLong()
return Money(id, amountInCents)
}
}
}
fun main() {
val moneyRepository = // repository implementation
// Store $10.99 (converted to 1099 cents)
val money = Money.fromAmount(1L, 10.99)
moneyRepository.save(money)
// Retrieve stored amount
val retrieved = moneyRepository.findById(1L).get()
println("Stored amount: $${retrieved.amount}")
// Perform accurate calculations
val money1 = Money.fromAmount(2L, 0.1)
val money2 = Money.fromAmount(3L, 0.2)
// Calculate in cents then convert
val sumInCents = money1.amountInCents + money2.amountInCents
val sum = sumInCents / 100.0
println("0.1 + 0.2 = $sum") // Prints exactly 0.3
}
The second approach leverages the BigDecimal class provided by Java/Kotlin to solve precision issues. BigDecimal offers exact decimal representation and naturally maps to the DECIMAL type in databases.
import java.math.BigDecimal
import java.math.RoundingMode
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Id
@Entity
class Product(
@Id
val id: Long,
val name: String,
// Maps to DECIMAL type
@Column(precision = 19, scale = 4)
val price: BigDecimal
)
// Extension function for price calculations
fun BigDecimal.applyTax(taxRate: BigDecimal): BigDecimal {
return this.multiply(taxRate.add(BigDecimal.ONE))
.setScale(2, RoundingMode.HALF_UP)
}
import java.math.BigDecimal
import java.math.RoundingMode
fun main() {
val productRepository = // repository implementation
// Exact decimal representation
val price = BigDecimal("10.99")
val product = Product(1L, "Refrigerator", price)
productRepository.save(product)
// Precise decimal operations
val num1 = BigDecimal("0.1")
val num2 = BigDecimal("0.2")
val sum = num1.add(num2)
println("0.1 + 0.2 = $sum")
println("Is sum equal to 0.3? ${sum.compareTo(BigDecimal(\"0.3\")) == 0}")
// Calculate 10% tax
val taxRate = BigDecimal("0.1")
val priceWithTax = price.applyTax(taxRate)
println("Price with tax: $priceWithTax")
}
Important Note: Always create BigDecimal from String rather than double literals to avoid precision issues before they even start. Using BigDecimal(0.1)
would inherit the double's imprecision, while BigDecimal("0.1")
maintains exact precision.
The third approach is storing numeric values as strings in the database. This method ensures perfect preservation of the original value, but requires conversion for mathematical operations.
import java.math.BigDecimal
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Id
@Entity
class StringAmount(
@Id
val id: Long,
val description: String,
// Store the exact value as a string
@Column(name = "amount_value")
val amountValue: String
) {
// Convert to BigDecimal when needed for calculations
fun toBigDecimal(): BigDecimal = BigDecimal(amountValue)
// Helper for addition with another StringAmount
fun add(other: StringAmount): String {
val result = this.toBigDecimal().add(other.toBigDecimal())
return result.toString()
}
// Helper for multiplication
fun multiply(factor: String): String {
val result = this.toBigDecimal().multiply(BigDecimal(factor))
return result.toString()
}
}
fun main() {
val amountRepository = // repository implementation
// Store exact decimal values as strings
val amount1 = StringAmount(1L, "Payment", "0.1")
val amount2 = StringAmount(2L, "Refund", "0.2")
amountRepository.save(amount1)
amountRepository.save(amount2)
// Calculate sum by converting to BigDecimal
val sum = amount1.add(amount2)
println("0.1 + 0.2 = $sum") // Exactly "0.3"
// Even extremely precise values maintain their exact representation
val preciseAmount = StringAmount(
3L,
"Scientific measurement",
"0.1234567890123456789012345678901234567890"
)
amountRepository.save(preciseAmount)
// Retrieve with full precision intact
val retrieved = amountRepository.findById(3L).get()
println("Retrieved value: ${retrieved.amountValue}")
}
When dealing with floating-point precision issues in Kotlin, choose the approach that best aligns with your specific requirements:
Remember: The choice of precision strategy isn't just a technical decision—it can have real financial and business implications. Take time to understand your requirements and choose the appropriate solution for your specific use case.