The Trade-offs Between toString() and toPlainString() in BigDecimal
Introduction
In financial applications, precise decimal representation is crucial. Recently, our team debated the use of toString()
versus toPlainString()
when working with BigDecimal values. This article explores the performance implications and practical considerations of both methods.
Performance Analysis
To quantify the performance difference, we conducted a benchmark comparing both methods across various number types:
val numbers = listOf(
BigDecimal("12345.56789"), // Regular number
BigDecimal("1234567890.0123456789"), // Regular number with high precision
BigDecimal("1E20"), // Very large number
BigDecimal("1E-8"), // Very small number
BigDecimal("999999999999999999.999999999999999999") // Large precision
)
val iterations = 1_000_000
numbers.forEach { number ->
println("\nTesting with number: $number")
val toStringTime = measureNanoTime {
repeat(iterations) {
number.toString()
}
}
val toPlainStringTime = measureNanoTime {
repeat(iterations) {
number.toPlainString()
}
}
println("toString() took: ${toStringTime / iterations} ns per call")
println("toPlainString() took: ${toPlainStringTime / iterations} ns per call")
println("toPlainString() is ${toPlainStringTime.toFloat() / toStringTime.toFloat()}x slower")
}
Results
The performance difference is significant and varies based on the number of digits:
Testing with number: 12345.56789
toString() took: 3 ns per call
toPlainString() took: 44 ns per call
toPlainString() is 11.48x slower
Testing with number: 1234567890.0123456789
toString() took: 3 ns per call
toPlainString() took: 241 ns per call
toPlainString() is 63.91x slower
Testing with number: 1E+20
toString() took: 1 ns per call
toPlainString() took: 61 ns per call
toPlainString() is 48.25x slower
Real-world Implications
In our banking application, we frequently need to display the difference between two BigDecimal values, typically for scenarios like:
- Showing account balance
- Calculating required additional deposits for purchases
- Displaying transaction amounts
Edge Case Analysis
We tested various scenarios to understand when scientific notation might appear:
val a = BigDecimal("1")
val b = BigDecimal("0.00000000000000100001")
val result = a - b
println("1 - 0.00000000000000100001 = ${result.toString()} (Plain: ${result.toPlainString()})")
val largeA = BigDecimal("1000000000000000000000")
val largeB = BigDecimal("999999999999999999999")
val largeResult = largeA - largeB
println("Large number subtraction = ${largeResult.toString()} (Plain: ${largeResult.toPlainString()})")
Our tests revealed that toString()
produces scientific notation primarily with very small differences:
Very small difference:
1.000000000000000000001 - 1 = 1E-21 (Plain: 0.000000000000000000001)
The Solution
To balance performance with user experience, we implemented a hybrid approach:
fun BigDecimal.toStringWithoutE(): String {
val asString = this.toString()
return if (asString.contains('E')) {
this.toPlainString()
} else {
asString
}
}
This solution:
- Initially uses the faster
toString()
- Falls back to
toPlainString()
only when scientific notation is detected - Ensures readable output for users while maintaining optimal performance in most cases
Conclusion
While toPlainString()
is significantly slower than toString()
, the hybrid approach provides an excellent balance between performance and usability. For financial applications where user experience is crucial, this compromise ensures both efficient processing and clear, understandable number representation.