Summary
Deci(Double) rejects every value whose magnitude is at or above 1e7, or below 1e-3.
All six actual constructors are this(value.toString()), and Kotlin switches Double.toString() to scientific notation at those thresholds. DECIMAL_REGEX has no exponent branch, so validateAndNormalizeDecimalLiteral rejects the literal the platform just produced.
Ten million of any currency is an ordinary amount, so in a financial codebase — which is what the library is for — this is reachable from normal data rather than from an edge case.
Reproduction
Against main (b8648e1), :deci:jvmTest:
for (v in listOf(9_999_999.0, 10_000_000.0, 29_638_000.0, 1.0e-3, 1.0e-4, -1.5e8)) {
println("$v -> toString='$v' -> " + runCatching { Deci(v).toString() })
}
9999999.0 -> toString='9999999.0' -> Success(9999999.0)
1.0E7 -> toString='1.0E7' -> Failure(DeciParseException: Invalid decimal literal: '1.0E7')
2.9638E7 -> toString='2.9638E7' -> Failure(DeciParseException: Invalid decimal literal: '2.9638E7')
0.001 -> toString='0.001' -> Success(0.001)
1.0E-4 -> toString='1.0E-4' -> Failure(DeciParseException: Invalid decimal literal: '1.0E-4')
-1.5E8 -> toString='-1.5E8' -> Failure(DeciParseException: Invalid decimal literal: '-1.5E8')
9999999.0 succeeds and 10000000.0 throws — the boundary is exactly where Double.toString() changes format, not anything about the value.
Double.toDeci() in DeciExtensions is Deci(this), so it carries the same behaviour.
Expected
Deci(Double) is total for every finite Double. The KDoc documents @throws DeciParseException on the String constructor only; on the Double constructor it warns about float artifacts, which reads as "the value may be imprecise", not "the call may fail".
Impact
Found in a POS application: an order list mapped price: Double from a JSON response into Deci and the whole screen fell into its server-error state on a Rp 29.638.000 transaction. Worth noting the failure is not always loud — where the value reaches a formatter that catches broadly, the exception is swallowed and the raw 2.9638E7 is rendered where a price belongs.
Suggested fix
Expand the exponent on the Double path before validating, leaving the String constructor's accepted grammar untouched. That keeps Deci("1e5") rejected as it is today, which feels like a separate API decision.
The alternative — teaching DECIMAL_REGEX an exponent branch — would fix this too, but it widens the accepted input surface for every caller, so I did not assume it.
PR follows.
Environment
- Deci 0.3.0 and
main (b8648e1)
- Kotlin Multiplatform, reproduced on the JVM target; the constructor is identical on all six
Summary
Deci(Double)rejects every value whose magnitude is at or above1e7, or below1e-3.All six
actualconstructors arethis(value.toString()), and Kotlin switchesDouble.toString()to scientific notation at those thresholds.DECIMAL_REGEXhas no exponent branch, sovalidateAndNormalizeDecimalLiteralrejects the literal the platform just produced.Ten million of any currency is an ordinary amount, so in a financial codebase — which is what the library is for — this is reachable from normal data rather than from an edge case.
Reproduction
Against
main(b8648e1),:deci:jvmTest:9999999.0succeeds and10000000.0throws — the boundary is exactly whereDouble.toString()changes format, not anything about the value.Double.toDeci()inDeciExtensionsisDeci(this), so it carries the same behaviour.Expected
Deci(Double)is total for every finiteDouble. The KDoc documents@throws DeciParseExceptionon theStringconstructor only; on theDoubleconstructor it warns about float artifacts, which reads as "the value may be imprecise", not "the call may fail".Impact
Found in a POS application: an order list mapped
price: Doublefrom a JSON response intoDeciand the whole screen fell into its server-error state on a Rp 29.638.000 transaction. Worth noting the failure is not always loud — where the value reaches a formatter that catches broadly, the exception is swallowed and the raw2.9638E7is rendered where a price belongs.Suggested fix
Expand the exponent on the
Doublepath before validating, leaving theStringconstructor's accepted grammar untouched. That keepsDeci("1e5")rejected as it is today, which feels like a separate API decision.The alternative — teaching
DECIMAL_REGEXan exponent branch — would fix this too, but it widens the accepted input surface for every caller, so I did not assume it.PR follows.
Environment
main(b8648e1)