Developer Playground
Solving Floating-Point Precision Issues with Kotlin
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.
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:
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.
Advertisement