Skip to content

fix: expand scientific notation in the Double constructor - #7

Open
DXMHXR wants to merge 3 commits into
Kimplify:mainfrom
DXMHXR:fix/double-constructor-scientific-notation
Open

DXMHXR wants to merge 3 commits into
Kimplify:mainfrom
DXMHXR:fix/double-constructor-scientific-notation

Conversation

@DXMHXR

@DXMHXR DXMHXR commented Sep 22, 2026 •

Copy link
Copy Markdown

Closes #6

Summary

Deci(Double) threw DeciParseException for every magnitude at or above 1e7, and below 1e-3.

All six actual constructors are this(value.toString()), and Kotlin switches Double.toString() to scientific notation at exactly those thresholds — 29638000.0 prints as 2.9638E7. DECIMAL_REGEX has no exponent branch, so validateAndNormalizeDecimalLiteral rejected the literal the platform had just produced. 9999999.0 was accepted and 10000000.0 was not; the boundary was the formatting change, not the value. Double.toDeci() inherited it.

Double.toDecimalLiteral() (internal, commonMain, pure Kotlin) expands the exponent in place before the value reaches the String constructor, and all six actuals now use it. NaN and the infinities carry no exponent marker, pass through unchanged, and are still rejected.

No change to the String constructor's accepted grammar — Deci("1e5") is rejected as before. Teaching DECIMAL_REGEX an exponent branch would fix this too, but it widens the input surface for every caller, so I left that as a separate decision for you.

No public API change, so the ABI dump is untouched.

Before / after

Deci(29_638_000.0)   // was: DeciParseException: Invalid decimal literal: '2.9638E7'
                     // now: 29638000
Deci(1.0e-4)         // was: DeciParseException: Invalid decimal literal: '1.0E-4'
                     // now: 0.00010
Deci(Double.NaN)     // unchanged: DeciParseException
Deci("1e5")          // unchanged: DeciParseException

Changelog

  • Deci(Double) no longer throws on ordinary magnitudes. Kotlin renders a Double in scientific notation once its magnitude reaches 1e7, or falls below 1e-3 — 29638000.0 prints as 2.9638E7. That literal is outside the grammar DECIMAL_REGEX accepts, so the Double constructor rejected it with DeciParseException: every value from ten million up, which for a currency amount is not an edge case. The constructor now expands the exponent before validating, on all six targets. NaN and the infinities are still rejected, and the String constructor's accepted grammar is unchanged — scientific notation is expanded only on the Double path.
  • Apple: values outside the native decimal range are rejected instead of becoming NaN. NSDecimalNumber holds a narrower range than a Double (or a long decimal literal), so on Apple targets Deci(Double.MAX_VALUE) used to produce NaN and very small magnitudes collapsed to zero. Both the Double and String constructors now throw DeciOverflowException for such input on Apple. JVM, Android, JS and WasmJs are unaffected.

Test Plan

Two new test classes in commonTest:

  • parser/DoubleLiteralTest — table-driven over expandScientificNotation, covering both exponent signs, e/E, an explicit + exponent (1e+21, the JS form), a non-integer exponent, and the non-finite literals. Assertions on toDecimalLiteral check only that no exponent marker survives, since Double.toString() itself differs per target and an exact-string expectation would not be portable.

  • DeciDoubleConstructorTest — the constructor's own behaviour, asserted against string-constructed Deci values per the repo's testing conventions: sign handling, arithmetic on expanded values, and that non-finite input still fails.

  • iosTest/DeciDoubleConstructorIosTest (added by @Merkost) — values outside NSDecimalNumber's range (Double.MAX_VALUE, Double.MIN_VALUE, a 309-digit literal) throw DeciOverflowException on Apple, and ordinary scientific-notation input is accepted.

  • JVM tests pass (./gradlew :deci:jvmTest)

  • JS tests pass (./gradlew :deci:jsTest)

  • WasmJs tests pass (./gradlew :deci:wasmJsTest)

  • Lint passes (./gradlew :deci:ktlintCheck)

  • API compatibility verified (./gradlew :deci:checkKotlinAbi)

  • iOS — :deci:compileKotlinIosSimulatorArm64 and :deci:compileTestKotlinIosSimulatorArm64 pass, but linkDebugTestIosSimulatorArm64 fails on my machine with Failed to build cache for kotlinx-serialization-core-iosSimulatorArm64Main-1.11.0.klib. That reproduces on unmodified main, so it is a local Kotlin/Native cache problem rather than anything in this change — leaving it to CI's clean runner.

How this was found

A POS app mapping price: Double off a JSON response into Deci: the order list dropped into its server-error state on a Rp 29.638.000 transaction. The quieter half is worth flagging — where such a value reaches a formatter that catches broadly, the exception is swallowed and the raw 2.9638E7 is rendered where a price belongs.

DXMHXR and others added 2 commits September 22, 2026 23:32
Kotlin renders a Double in scientific notation once its magnitude reaches 1e7,
or falls below 1e-3, so 29638000.0 prints as 2.9638E7. DECIMAL_REGEX has no
exponent branch, so validateAndNormalizeDecimalLiteral rejected the literal the
platform had just produced and Deci(Double) threw DeciParseException for every
value from ten million up. 9999999.0 was accepted and 10000000.0 was not: the
boundary sat exactly where Double.toString() changes format.

Double.toDecimalLiteral() expands the exponent in place before the value reaches
the String constructor, and all six actuals now use it. NaN and the infinities
carry no exponent marker, pass through unchanged and are still rejected.

The String constructor's accepted grammar is untouched — Deci("1e5") is rejected
as before. Teaching DECIMAL_REGEX an exponent branch would have fixed this too,
but it widens the input surface for every caller, which is a separate decision.

Closes Kimplify#6
@Merkost

Merkost commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Thanks for the fix. The iOS test exposed an edge case: Deci(Double.MAX_VALUE).toDouble() returns NaN. Let’s agree on the expected behavior for values outside iOS’s decimal range and address it before merging.

@Merkost
Merkost force-pushed the fix/double-constructor-scientific-notation branch from 6831442 to b177056 Compare September 24, 2026 04:18
@DXMHXR

DXMHXR commented Sep 24, 2026

Copy link
Copy Markdown
Author

Thanks for catching that and for the fix. Agreed: throwing DeciOverflowException beats silently producing NaN or 0, and it's consistent with what this PR is fixing.

Two small things:

  • parseRepresentableDecimal also changes the String constructor on Apple — Deci("1" + "0".repeat(308)) now throws instead of yielding NaN. Worth a line in the CHANGELOG, since my entry says the String path is unchanged.
  • I've updated the PR description: the MAX_VALUE/MIN_VALUE coverage now lives in your iOS test rather than the common one, and the changelog section mentions the Apple range check.

Separately, literals with more than 38 significant digits are still rounded silently on Apple. Out of scope here, but happy to open an issue if you'd like it tracked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deci(Double) throws DeciParseException for any magnitude >= 1e7

2 participants