1. Welcome Developer Playground by Giri

Developer Playground

Solving Floating-Point Precision Issues with Kotlin

Updated: April 23, 2025

Understanding Floating-Point Precision Problems

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.

  • Precision loss due to binary floating-point representation limitations
  • Accumulation of rounding errors
  • Type conversion issues when storing and retrieving from databases
  • Accuracy requirements in currency and financial calculations

Floating-point problem example:

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.

Solutions to Floating-Point Precision ProblemsInteger ConversionDecimal Type UsageString StorageMultiply by 10^n for integerSimple and fast operationsReduced readabilityPotential for overflowUsing BigDecimalGuaranteed precisionPotential performance hitCompatible with DB DECIMALStore values as stringsPerfect precision preservationConversion needed for operationsIndexing/sorting constraintsSolution ComparisonPerformance ⭐⭐⭐⭐Precision ⭐⭐⭐⭐⭐Storage efficiency ⭐⭐⭐Implementation: EasyImplementation: MediumImplementation: Easy

Solving Precision Issues with Integer Conversion

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.

Implementation:

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)
        }
    }
}

Usage Example:

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
}

Advantages:

  • Simple and intuitive implementation
  • Integer operations are fast and efficient
  • Efficiently processed in most databases

Disadvantages:

  • Only supports fixed decimal places (e.g., cents or 2 decimal places)
  • May exceed Long range for very large values
  • Conversion logic must always be kept in mind in code

Solving Precision Issues with BigDecimal

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.

Implementation:

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)
}

Usage Example:

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.

Advantages:

  • Provides arbitrary precision for decimal calculations
  • Directly maps to database DECIMAL types
  • Includes built-in rounding control
  • Best option for financial calculations

Disadvantages:

  • Operations are slower than primitive number types
  • Immutable objects create more garbage collection pressure
  • More verbose API compared to primitive operations

String Storage Approach

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.

Implementation:

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()
    }
}

    

Usage Example:

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}")
}

Advantages:

  • Perfect preservation of original value with unlimited precision
  • No information loss during storage or retrieval
  • Simple implementation with string data types
  • Works well for values with extreme precision requirements

Disadvantages:

  • Requires conversion to numeric types for calculations
  • More storage space required for large numbers
  • Less efficient for numeric indexing and sorting in databases
  • Need for validation to ensure stored strings are valid numbers

Conclusion and Best Practices

When dealing with floating-point precision issues in Kotlin, choose the approach that best aligns with your specific requirements:

Decision Matrix:

  • Use the Integer Conversion Approach when: Working with fixed decimal places (like money) and performance is critical
  • Use the BigDecimal Approach when: Working with financial calculations or when database precision types align with your needs
  • Use the String Storage Approach when: Maximum precision is required and the values may have arbitrary decimal places

Best Practices:

  • Always create BigDecimal from strings, not from floating-point literals
  • Document your precision strategy clearly in your codebase
  • Use appropriate scale and rounding modes for financial calculations
  • Write comprehensive tests specifically targeting precision issues
  • Consider creating domain-specific types for handling important numeric values

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.

Copyright © 2025 Giri Labs Inc.·Trademark Policy