From 5d6313737029a8407ccabf6b7b6e60295c34d300 Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 10:37:12 +1100 Subject: [PATCH 01/19] Remove stripTrailingZeros from JVM and Android constructors Preserve trailing zeros in Deci string representation to maintain scale fidelity for financial use cases (e.g. "1.50" stays "1.50"). Co-Authored-By: Claude Opus 4.6 (1M context) --- deci/src/androidMain/kotlin/org/kimplify/deci/Deci.kt | 2 +- deci/src/jvmMain/kotlin/org/kimplify/deci/Deci.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deci/src/androidMain/kotlin/org/kimplify/deci/Deci.kt b/deci/src/androidMain/kotlin/org/kimplify/deci/Deci.kt index d7129eb..8756e14 100644 --- a/deci/src/androidMain/kotlin/org/kimplify/deci/Deci.kt +++ b/deci/src/androidMain/kotlin/org/kimplify/deci/Deci.kt @@ -15,7 +15,7 @@ actual class Deci( private val internal: BigDecimal, ) : Comparable { actual constructor(value: String) : this( - BigDecimal(validateAndNormalizeDecimalLiteral(value)).stripTrailingZeros(), + BigDecimal(validateAndNormalizeDecimalLiteral(value)), ) actual constructor(value: Long) : this(value.toString()) diff --git a/deci/src/jvmMain/kotlin/org/kimplify/deci/Deci.kt b/deci/src/jvmMain/kotlin/org/kimplify/deci/Deci.kt index d7129eb..8756e14 100644 --- a/deci/src/jvmMain/kotlin/org/kimplify/deci/Deci.kt +++ b/deci/src/jvmMain/kotlin/org/kimplify/deci/Deci.kt @@ -15,7 +15,7 @@ actual class Deci( private val internal: BigDecimal, ) : Comparable { actual constructor(value: String) : this( - BigDecimal(validateAndNormalizeDecimalLiteral(value)).stripTrailingZeros(), + BigDecimal(validateAndNormalizeDecimalLiteral(value)), ) actual constructor(value: Long) : this(value.toString()) From 3e4894135b7360483cb9786e937ecd2e1413a3ff Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 11:31:47 +1100 Subject: [PATCH 02/19] Add design spec for removing toPlainString and unifying toString Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-27-remove-toplainstring-design.md | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-27-remove-toplainstring-design.md diff --git a/docs/superpowers/specs/2026-03-27-remove-toplainstring-design.md b/docs/superpowers/specs/2026-03-27-remove-toplainstring-design.md new file mode 100644 index 0000000..560e3b3 --- /dev/null +++ b/docs/superpowers/specs/2026-03-27-remove-toplainstring-design.md @@ -0,0 +1,77 @@ +# Remove `toPlainString()` — Unify String Representation + +**Date:** 2026-03-27 +**Status:** Approved + +## Problem + +Deci has two string conversion methods: `toString()` (may use scientific notation on JVM/Android) and `toPlainString()` (never scientific notation). This creates confusion and cross-platform inconsistency. For a financial library, scientific notation is never appropriate, and having two methods that *should* behave identically is unnecessary API surface. + +Additionally, the JVM/Android `stripTrailingZeros()` call in the constructor has already been removed (on the `fix/remove-strip-trailing-zeros` branch), so trailing zeros are now preserved. + +## Decision + +Remove `toPlainString()` entirely. Make `toString()` the single string representation method, and guarantee it never uses scientific notation on any platform. + +## Changes + +### 1. API Surface (commonMain) + +- Remove `fun toPlainString(): String` from the `expect` declaration in `commonMain/Deci.kt` +- Update `toString()` KDoc: remove the scientific notation caveat, state it always returns plain decimal form preserving scale + +### 2. Platform Implementations + +**JVM (`jvmMain/Deci.kt`) and Android (`androidMain/Deci.kt`):** +- Remove `actual fun toPlainString()` +- Change `toString()` from `internal.toString()` to `internal.toPlainString()` (BigDecimal's method) + +**JS (`jsMain/Deci.kt`) and wasmJs (`wasmJsMain/Deci.kt`):** +- Remove `actual fun toPlainString()` +- `toString()` unchanged (already never uses scientific notation) + +**Apple (`appleMain/Deci.kt`):** +- Remove `actual fun toPlainString()` +- `toString()` unchanged (already never uses scientific notation) + +### 3. Internal Call Site Migrations (`toPlainString()` -> `toString()`) + +All these files call `Deci.toPlainString()` and must switch to `toString()`: + +- `DeciSerializer.kt:30` +- `DeciFormatting.kt:37, 69, 91, 141, 143, 159, 257` +- `DeciValidation.kt:66, 118, 237, 246` +- `DeciMath.kt:155` +- `DeciExtensions.kt:34` + +**Note:** JVM/Android files that call `BigDecimal.toPlainString()` (e.g., in `operate()`, `divide()`, `abs()`, `negate()`) are calling Java's method, not Deci's. These do not change. + +### 4. Test Updates + +- `DeciTest.kt:261` — `d.toPlainString()` -> `d.toString()` +- `DeciTest.kt:285-286` — flip assertion: `Deci("1.2300").toString()` should equal `"1.2300"` (trailing zeros preserved) +- `DeciTest.kt:289-291` — rename test to "toString never uses scientific notation", change `toPlainString()` calls to `toString()` +- `DeciTest.kt:294-297` — delete "toString may use scientific notation" test +- `DeciPropertyTest.kt:207` — `a.toPlainString()` -> `a.toString()` +- `DeciSerializationTest.kt:83` — `restored.toPlainString()` -> `restored.toString()` +- `DeciMathExtendedTest.kt:268` — `result.toPlainString()` -> `result.toString()` +- `DeciBulkOperationsTest.kt:267` — `result[0].toPlainString()` -> `result[0].toString()` + +### 5. Sample App + +- `ValidationScreen.kt:284` — two `.toPlainString()` calls -> `.toString()` + +### 6. Documentation + +- `commonMain/Deci.kt` KDoc on `toString()` — remove scientific notation language, document plain decimal form +- `DeciSerializer.kt` KDoc — remove `toPlainString` reference, say `toString()` is used +- README.MD and CLAUDE.md have no direct references to `toPlainString`, no changes needed + +### 7. API Compatibility + +- Run `./gradlew apiDump` after all changes to update the `.api` file (removing `toPlainString` from the public API) + +## Out of Scope + +- Changing how arithmetic propagates scale (e.g., what scale `Deci("1.50") + Deci("2.5")` produces) — that's a separate concern +- Adding locale-aware formatting — `toString()` always uses `.` as separator; locale formatting belongs in `DeciFormatting` From 78cbe68bd64c54bca3177a20d50b6995a3c233b9 Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 11:35:28 +1100 Subject: [PATCH 03/19] Add implementation plan for removing toPlainString Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-03-27-remove-toplainstring.md | 499 ++++++++++++++++++ 1 file changed, 499 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-27-remove-toplainstring.md diff --git a/docs/superpowers/plans/2026-03-27-remove-toplainstring.md b/docs/superpowers/plans/2026-03-27-remove-toplainstring.md new file mode 100644 index 0000000..57a9e5a --- /dev/null +++ b/docs/superpowers/plans/2026-03-27-remove-toplainstring.md @@ -0,0 +1,499 @@ +# Remove `toPlainString()` — Unify String Representation + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Remove `toPlainString()` from the Deci API and make `toString()` the single string representation method that never uses scientific notation. + +**Architecture:** Remove `toPlainString()` from the expect declaration and all actuals. On JVM/Android, switch `toString()` to delegate to `BigDecimal.toPlainString()`. Migrate all internal and test call sites from `toPlainString()` to `toString()`. + +**Tech Stack:** Kotlin Multiplatform, BigDecimal (JVM), DecimalJS (JS/wasmJs), NSDecimalNumber (Apple) + +--- + +### Task 1: Remove `toPlainString()` from expect declaration and update `toString()` KDoc + +**Files:** +- Modify: `deci/src/commonMain/kotlin/org/kimplify/deci/Deci.kt:131-148` + +- [ ] **Step 1: Remove `toPlainString()` and update `toString()` KDoc** + +Replace lines 131-148 with: + +```kotlin + + /** + * Returns the string representation of this [Deci] without scientific notation, + * preserving the scale (e.g. `"1.50"` stays `"1.50"`). + * + * This method never uses exponential notation regardless of the value's magnitude. + */ + override fun toString(): String +``` + +- [ ] **Step 2: Verify commonMain compiles** + +Run: `./gradlew :deci:compileCommonMainKotlinMetadata` +Expected: Compilation errors in actuals and call sites (expected at this stage) + +- [ ] **Step 3: Commit** + +```bash +git add deci/src/commonMain/kotlin/org/kimplify/deci/Deci.kt +git commit -m "Remove toPlainString from expect declaration, update toString KDoc" +``` + +--- + +### Task 2: Update JVM actual — remove `toPlainString()`, fix `toString()` + +**Files:** +- Modify: `deci/src/jvmMain/kotlin/org/kimplify/deci/Deci.kt:106-108` + +- [ ] **Step 1: Remove `toPlainString()` and change `toString()` to use `BigDecimal.toPlainString()`** + +Replace: + +```kotlin + actual override fun toString(): String = internal.toString() + + actual fun toPlainString(): String = internal.toPlainString() +``` + +With: + +```kotlin + actual override fun toString(): String = internal.toPlainString() +``` + +- [ ] **Step 2: Commit** + +```bash +git add deci/src/jvmMain/kotlin/org/kimplify/deci/Deci.kt +git commit -m "JVM: remove toPlainString, make toString use BigDecimal.toPlainString" +``` + +--- + +### Task 3: Update Android actual — remove `toPlainString()`, fix `toString()` + +**Files:** +- Modify: `deci/src/androidMain/kotlin/org/kimplify/deci/Deci.kt:106-108` + +- [ ] **Step 1: Remove `toPlainString()` and change `toString()` to use `BigDecimal.toPlainString()`** + +Replace: + +```kotlin + actual override fun toString(): String = internal.toString() + + actual fun toPlainString(): String = internal.toPlainString() +``` + +With: + +```kotlin + actual override fun toString(): String = internal.toPlainString() +``` + +- [ ] **Step 2: Commit** + +```bash +git add deci/src/androidMain/kotlin/org/kimplify/deci/Deci.kt +git commit -m "Android: remove toPlainString, make toString use BigDecimal.toPlainString" +``` + +--- + +### Task 4: Update JS actual — remove `toPlainString()` + +**Files:** +- Modify: `deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt:85-88` + +- [ ] **Step 1: Remove `toPlainString()`** + +Remove these lines: + +```kotlin + actual fun toPlainString(): String { + val scale = _scale + return if (scale != null) internal.toFixed(scale) else internal.toFixed() + } +``` + +- [ ] **Step 2: Commit** + +```bash +git add deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt +git commit -m "JS: remove toPlainString" +``` + +--- + +### Task 5: Update wasmJs actual — remove `toPlainString()` + +**Files:** +- Modify: `deci/src/wasmJsMain/kotlin/org/kimplify/deci/Deci.kt:85-88` + +- [ ] **Step 1: Remove `toPlainString()`** + +Remove these lines: + +```kotlin + actual fun toPlainString(): String { + val scale = _scale + return if (scale != null) internal.toFixed(scale) else internal.toFixed() + } +``` + +- [ ] **Step 2: Commit** + +```bash +git add deci/src/wasmJsMain/kotlin/org/kimplify/deci/Deci.kt +git commit -m "wasmJs: remove toPlainString" +``` + +--- + +### Task 6: Update Apple actual — remove `toPlainString()` + +**Files:** +- Modify: `deci/src/appleMain/kotlin/org/kimplify/deci/Deci.kt:174-182` + +- [ ] **Step 1: Remove `toPlainString()`** + +Remove these lines: + +```kotlin + actual fun toPlainString(): String { + val str = internal.stringValue + val scale = _scale ?: return str + if (scale == 0) return str.split(".")[0] + val parts = str.split(".") + val intPart = parts[0] + val fracPart = if (parts.size > 1) parts[1] else "" + return "$intPart.${fracPart.padEnd(scale, '0')}" + } +``` + +- [ ] **Step 2: Commit** + +```bash +git add deci/src/appleMain/kotlin/org/kimplify/deci/Deci.kt +git commit -m "Apple: remove toPlainString" +``` + +--- + +### Task 7: Migrate internal call sites from `toPlainString()` to `toString()` + +**Files:** +- Modify: `deci/src/commonMain/kotlin/org/kimplify/deci/DeciSerializer.kt:30` +- Modify: `deci/src/commonMain/kotlin/org/kimplify/deci/formatting/DeciFormatting.kt:37,69,91,141,143,159,257` +- Modify: `deci/src/commonMain/kotlin/org/kimplify/deci/validation/DeciValidation.kt:66,118,237,246` +- Modify: `deci/src/commonMain/kotlin/org/kimplify/deci/math/DeciMath.kt:155` +- Modify: `deci/src/commonMain/kotlin/org/kimplify/deci/extension/DeciExtensions.kt:34` + +- [ ] **Step 1: Update DeciSerializer** + +In `DeciSerializer.kt` line 30, replace: + +```kotlin + encoder.encodeString(value.toPlainString()) +``` + +With: + +```kotlin + encoder.encodeString(value.toString()) +``` + +Also update the class KDoc — replace: + +```kotlin + * For example, `Deci("1.50")` is serialized as the JSON string `"1.50"`, not the + * JSON number `1.5`. + * + * Deserialization parses the decoded string via the [Deci] string constructor. +``` + +With: + +```kotlin + * For example, `Deci("1.50")` is serialized as the JSON string `"1.50"`, not the + * JSON number `1.5`. + * + * Serialization uses [Deci.toString], which never produces scientific notation. + * Deserialization parses the decoded string via the [Deci] string constructor. +``` + +- [ ] **Step 2: Update DeciFormatting** + +In `DeciFormatting.kt`, replace all `.toPlainString()` with `.toString()`. There are 7 occurrences: + +Line 37: `val str = this.toPlainString()` → `val str = this.toString()` +Line 69: `return "${rounded.toPlainString()}$symbol"` → `return "${rounded.toString()}$symbol"` +Line 91: `val str = this.abs().toPlainString()` → `val str = this.abs().toString()` +Line 141: `"0.00" -> this.setScale(2, RoundingMode.HALF_UP).toPlainString()` → `"0.00" -> this.setScale(2, RoundingMode.HALF_UP).toString()` +Line 143: `"0.0000" -> this.setScale(4, RoundingMode.HALF_UP).toPlainString()` → `"0.0000" -> this.setScale(4, RoundingMode.HALF_UP).toString()` +Line 159: `val parts = abs.toPlainString().split(".")` → `val parts = abs.toString().split(".")` +Line 257: `val str = this.toPlainString()` → `val str = this.toString()` + +- [ ] **Step 3: Update DeciValidation** + +In `DeciValidation.kt`, replace all `.toPlainString()` with `.toString()`. There are 4 occurrences: + +Line 66: `val str = this.toPlainString()` → `val str = this.toString()` +Line 118: `val str = this.toPlainString()` → `val str = this.toString()` +Line 237: `"Value must be at least ${min.toPlainString()}"` → `"Value must be at least ${min.toString()}"` +Line 246: `"Value must be at most ${max.toPlainString()}"` → `"Value must be at most ${max.toString()}"` + +- [ ] **Step 4: Update DeciMath** + +In `DeciMath.kt` line 155, replace: + +```kotlin + val str = absValue.toPlainString() +``` + +With: + +```kotlin + val str = absValue.toString() +``` + +- [ ] **Step 5: Update DeciExtensions** + +In `DeciExtensions.kt` line 34, replace: + +```kotlin + val str = truncated.toPlainString() +``` + +With: + +```kotlin + val str = truncated.toString() +``` + +- [ ] **Step 6: Commit** + +```bash +git add deci/src/commonMain/kotlin/org/kimplify/deci/DeciSerializer.kt \ + deci/src/commonMain/kotlin/org/kimplify/deci/formatting/DeciFormatting.kt \ + deci/src/commonMain/kotlin/org/kimplify/deci/validation/DeciValidation.kt \ + deci/src/commonMain/kotlin/org/kimplify/deci/math/DeciMath.kt \ + deci/src/commonMain/kotlin/org/kimplify/deci/extension/DeciExtensions.kt +git commit -m "Migrate all internal toPlainString calls to toString" +``` + +--- + +### Task 8: Update tests + +**Files:** +- Modify: `deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt:258-297` +- Modify: `deci/src/commonTest/kotlin/org/kimplify/deci/DeciPropertyTest.kt:207` +- Modify: `deci/src/commonTest/kotlin/org/kimplify/deci/DeciSerializationTest.kt:83` +- Modify: `deci/src/commonTest/kotlin/org/kimplify/deci/math/DeciMathExtendedTest.kt:268` +- Modify: `deci/src/commonTest/kotlin/org/kimplify/deci/bulk/DeciBulkOperationsTest.kt:267` + +- [ ] **Step 1: Update DeciTest.kt — fix roundtrip test** + +Replace: + +```kotlin + @Test fun `toDouble and toPlainString roundtrip for simple values`() { + listOf("0", "1.5", "-2.75").forEach { s -> + val d = Deci(s) + assertEquals(s, d.toPlainString()) + assertEquals(s.toDouble(), d.toDouble()) + } + } +``` + +With: + +```kotlin + @Test fun `toDouble and toString roundtrip for simple values`() { + listOf("0", "1.5", "-2.75").forEach { s -> + val d = Deci(s) + assertEquals(s, d.toString()) + assertEquals(s.toDouble(), d.toDouble()) + } + } +``` + +- [ ] **Step 2: Update DeciTest.kt — flip trailing zeros test** + +Replace: + +```kotlin + @Test fun `trailing zeros are stripped by constructor`() { + assertEquals("1.23", Deci("1.2300").toPlainString()) + } +``` + +With: + +```kotlin + @Test fun `trailing zeros are preserved by constructor`() { + assertEquals("1.2300", Deci("1.2300").toString()) + } +``` + +- [ ] **Step 3: Update DeciTest.kt — rename scientific notation test** + +Replace: + +```kotlin + @Test fun `toPlainString never uses scientific notation`() { + assertEquals("100000000000000000000", Deci("100000000000000000000").toPlainString()) + assertEquals("0.000000001", Deci("0.000000001").toPlainString()) + } +``` + +With: + +```kotlin + @Test fun `toString never uses scientific notation`() { + assertEquals("100000000000000000000", Deci("100000000000000000000").toString()) + assertEquals("0.000000001", Deci("0.000000001").toString()) + } +``` + +- [ ] **Step 4: Update DeciTest.kt — delete the old scientific notation test** + +Delete: + +```kotlin + @Test fun `toString may use scientific notation for extreme values`() { + val large = Deci("100000000000000000000") + assertEquals(large, Deci(large.toPlainString())) + } +``` + +- [ ] **Step 5: Update DeciPropertyTest.kt** + +Line 207, replace: + +```kotlin + val copy = Deci(a.toPlainString()) +``` + +With: + +```kotlin + val copy = Deci(a.toString()) +``` + +- [ ] **Step 6: Update DeciSerializationTest.kt** + +Line 83, replace: + +```kotlin + assertEquals(s, restored.toPlainString(), "String representation changed for $s") +``` + +With: + +```kotlin + assertEquals(s, restored.toString(), "String representation changed for $s") +``` + +- [ ] **Step 7: Update DeciMathExtendedTest.kt** + +Line 268, replace: + +```kotlin + assertTrue(result.toPlainString().startsWith("12")) +``` + +With: + +```kotlin + assertTrue(result.toString().startsWith("12")) +``` + +- [ ] **Step 8: Update DeciBulkOperationsTest.kt** + +Line 267, replace: + +```kotlin + assertTrue(result[0].toPlainString().length > 5) +``` + +With: + +```kotlin + assertTrue(result[0].toString().length > 5) +``` + +- [ ] **Step 9: Commit** + +```bash +git add deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt \ + deci/src/commonTest/kotlin/org/kimplify/deci/DeciPropertyTest.kt \ + deci/src/commonTest/kotlin/org/kimplify/deci/DeciSerializationTest.kt \ + deci/src/commonTest/kotlin/org/kimplify/deci/math/DeciMathExtendedTest.kt \ + deci/src/commonTest/kotlin/org/kimplify/deci/bulk/DeciBulkOperationsTest.kt +git commit -m "Update all tests: toPlainString -> toString, fix trailing zeros assertion" +``` + +--- + +### Task 9: Update sample app + +**Files:** +- Modify: `sample/composeApp/src/commonMain/kotlin/org/kimplify/screens/ValidationScreen.kt:284` + +- [ ] **Step 1: Replace `toPlainString()` call** + +Line 284, replace: + +```kotlin + original.toPlainString() == deserialized.toPlainString() +``` + +With: + +```kotlin + original.toString() == deserialized.toString() +``` + +- [ ] **Step 2: Commit** + +```bash +git add sample/composeApp/src/commonMain/kotlin/org/kimplify/screens/ValidationScreen.kt +git commit -m "Sample app: toPlainString -> toString" +``` + +--- + +### Task 10: Build, test, and update API dump + +- [ ] **Step 1: Build all targets** + +Run: `./gradlew build` +Expected: BUILD SUCCESSFUL + +- [ ] **Step 2: Run all tests** + +Run: `./gradlew allTests` +Expected: All tests pass + +- [ ] **Step 3: Update API dump** + +Run: `./gradlew apiDump` +Expected: `.api` file updated, `toPlainString` removed from public API + +- [ ] **Step 4: Verify API dump is clean** + +Run: `./gradlew apiCheck` +Expected: BUILD SUCCESSFUL + +- [ ] **Step 5: Commit API dump** + +```bash +git add deci/api/ +git commit -m "Update API dump: remove toPlainString from public API" +``` From d0bc282fcc80877090e425563aaca4fcd75c406b Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 11:50:07 +1100 Subject: [PATCH 04/19] Fix plan: JS/wasmJs toString must also avoid scientific notation DecimalJs.toString() can use scientific notation for extreme values. The plan now replaces toString() with toFixed()-based implementation on both JS and wasmJs platforms, not just removing toPlainString(). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-03-27-remove-toplainstring.md | 54 +++++++++++++++---- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/docs/superpowers/plans/2026-03-27-remove-toplainstring.md b/docs/superpowers/plans/2026-03-27-remove-toplainstring.md index 57a9e5a..ec2a6b9 100644 --- a/docs/superpowers/plans/2026-03-27-remove-toplainstring.md +++ b/docs/superpowers/plans/2026-03-27-remove-toplainstring.md @@ -104,52 +104,86 @@ git commit -m "Android: remove toPlainString, make toString use BigDecimal.toPla --- -### Task 4: Update JS actual — remove `toPlainString()` +### Task 4: Update JS actual — remove `toPlainString()`, fix `toString()` **Files:** -- Modify: `deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt:85-88` +- Modify: `deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt:79-88` -- [ ] **Step 1: Remove `toPlainString()`** +- [ ] **Step 1: Remove `toPlainString()` and fix `toString()` to never use scientific notation** -Remove these lines: +`DecimalJs.toString()` can use scientific notation for extreme values. Replace both methods with a single `toString()` that uses `toFixed()` (which never uses scientific notation): + +Replace: ```kotlin + actual override fun toString(): String { + val scale = _scale ?: return internal.toString() + if (scale <= 0) return internal.toString() + return internal.toFixed(scale) + } + actual fun toPlainString(): String { val scale = _scale return if (scale != null) internal.toFixed(scale) else internal.toFixed() } ``` +With: + +```kotlin + actual override fun toString(): String { + val scale = _scale + return if (scale != null && scale > 0) internal.toFixed(scale) else internal.toFixed() + } +``` + - [ ] **Step 2: Commit** ```bash git add deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt -git commit -m "JS: remove toPlainString" +git commit -m "JS: remove toPlainString, fix toString to never use scientific notation" ``` --- -### Task 5: Update wasmJs actual — remove `toPlainString()` +### Task 5: Update wasmJs actual — remove `toPlainString()`, fix `toString()` **Files:** -- Modify: `deci/src/wasmJsMain/kotlin/org/kimplify/deci/Deci.kt:85-88` +- Modify: `deci/src/wasmJsMain/kotlin/org/kimplify/deci/Deci.kt:79-88` -- [ ] **Step 1: Remove `toPlainString()`** +- [ ] **Step 1: Remove `toPlainString()` and fix `toString()` to never use scientific notation** -Remove these lines: +Same fix as JS — `DecimalJs.toString()` can use scientific notation. Replace both methods: + +Replace: ```kotlin + actual override fun toString(): String { + val scale = _scale ?: return internal.toString() + if (scale <= 0) return internal.toString() + return internal.toFixed(scale) + } + actual fun toPlainString(): String { val scale = _scale return if (scale != null) internal.toFixed(scale) else internal.toFixed() } ``` +With: + +```kotlin + actual override fun toString(): String { + val scale = _scale + return if (scale != null && scale > 0) internal.toFixed(scale) else internal.toFixed() + } +``` + - [ ] **Step 2: Commit** ```bash git add deci/src/wasmJsMain/kotlin/org/kimplify/deci/Deci.kt -git commit -m "wasmJs: remove toPlainString" +git commit -m "wasmJs: remove toPlainString, fix toString to never use scientific notation" ``` --- From f9947f871bb6ed29c7c25cd5d40c21cacfad9eae Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 13:05:32 +1100 Subject: [PATCH 05/19] Task 1: Remove toPlainString expect declaration; update toString KDoc to describe plain-string semantics Co-Authored-By: Claude Sonnet 4.6 --- .../src/commonMain/kotlin/org/kimplify/deci/Deci.kt | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/deci/src/commonMain/kotlin/org/kimplify/deci/Deci.kt b/deci/src/commonMain/kotlin/org/kimplify/deci/Deci.kt index d46dc20..dcdb691 100644 --- a/deci/src/commonMain/kotlin/org/kimplify/deci/Deci.kt +++ b/deci/src/commonMain/kotlin/org/kimplify/deci/Deci.kt @@ -129,23 +129,14 @@ expect class Deci : Comparable { roundingMode: RoundingMode, ): Deci - /** - * Returns the string representation of this [Deci]. - * - * For very large or very small values, the result may use scientific notation - * (e.g. `"1E+20"`) depending on the platform. Use [toPlainString] when a - * plain decimal string is required. - */ - override fun toString(): String /** * Returns the string representation of this [Deci] without scientific notation, * preserving the scale (e.g. `"1.50"` stays `"1.50"`). * - * Unlike [toString], this method never uses exponential notation regardless - * of the value's magnitude. + * This method never uses exponential notation regardless of the value's magnitude. */ - fun toPlainString(): String + override fun toString(): String /** * Converts this [Deci] to a [Double]. From b6386a25391c8786754ca057ec22df73464a89df Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 13:05:36 +1100 Subject: [PATCH 06/19] =?UTF-8?q?Task=202:=20JVM=20actual=20=E2=80=94=20de?= =?UTF-8?q?legate=20toString=20to=20BigDecimal.toPlainString(),=20remove?= =?UTF-8?q?=20toPlainString()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- deci/src/jvmMain/kotlin/org/kimplify/deci/Deci.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/deci/src/jvmMain/kotlin/org/kimplify/deci/Deci.kt b/deci/src/jvmMain/kotlin/org/kimplify/deci/Deci.kt index 8756e14..e22f0bd 100644 --- a/deci/src/jvmMain/kotlin/org/kimplify/deci/Deci.kt +++ b/deci/src/jvmMain/kotlin/org/kimplify/deci/Deci.kt @@ -103,9 +103,7 @@ actual class Deci( return Deci(internal.setScale(scale, convert(roundingMode))) } - actual override fun toString(): String = internal.toString() - - actual fun toPlainString(): String = internal.toPlainString() + actual override fun toString(): String = internal.toPlainString() actual fun toDouble(): Double = internal.toDouble() From 03f088b247c463b534dff00ee819f9bf25eb3889 Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 13:05:39 +1100 Subject: [PATCH 07/19] =?UTF-8?q?Task=203:=20Android=20actual=20=E2=80=94?= =?UTF-8?q?=20delegate=20toString=20to=20BigDecimal.toPlainString(),=20rem?= =?UTF-8?q?ove=20toPlainString()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- deci/src/androidMain/kotlin/org/kimplify/deci/Deci.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/deci/src/androidMain/kotlin/org/kimplify/deci/Deci.kt b/deci/src/androidMain/kotlin/org/kimplify/deci/Deci.kt index 8756e14..e22f0bd 100644 --- a/deci/src/androidMain/kotlin/org/kimplify/deci/Deci.kt +++ b/deci/src/androidMain/kotlin/org/kimplify/deci/Deci.kt @@ -103,9 +103,7 @@ actual class Deci( return Deci(internal.setScale(scale, convert(roundingMode))) } - actual override fun toString(): String = internal.toString() - - actual fun toPlainString(): String = internal.toPlainString() + actual override fun toString(): String = internal.toPlainString() actual fun toDouble(): Double = internal.toDouble() From 4aa2d72fe9205c43e35ad25089de9785f991a3d1 Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 13:05:42 +1100 Subject: [PATCH 08/19] =?UTF-8?q?Task=204:=20JS=20actual=20=E2=80=94=20uni?= =?UTF-8?q?fy=20toString=20to=20always=20use=20toFixed(),=20remove=20toPla?= =?UTF-8?q?inString()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt b/deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt index c9ed747..ca3f905 100644 --- a/deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt +++ b/deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt @@ -77,14 +77,8 @@ actual class Deci private constructor( } actual override fun toString(): String { - val scale = _scale ?: return internal.toString() - if (scale <= 0) return internal.toString() - return internal.toFixed(scale) - } - - actual fun toPlainString(): String { val scale = _scale - return if (scale != null) internal.toFixed(scale) else internal.toFixed() + return if (scale != null && scale > 0) internal.toFixed(scale) else internal.toFixed() } actual fun toDouble(): Double = internal.toNumber() From acb93b4f14175a9423755fea4156d2cdc46a4e5f Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 13:05:46 +1100 Subject: [PATCH 09/19] =?UTF-8?q?Task=205:=20wasmJs=20actual=20=E2=80=94?= =?UTF-8?q?=20unify=20toString=20to=20always=20use=20toFixed(),=20remove?= =?UTF-8?q?=20toPlainString()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- deci/src/wasmJsMain/kotlin/org/kimplify/deci/Deci.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/deci/src/wasmJsMain/kotlin/org/kimplify/deci/Deci.kt b/deci/src/wasmJsMain/kotlin/org/kimplify/deci/Deci.kt index c9ed747..ca3f905 100644 --- a/deci/src/wasmJsMain/kotlin/org/kimplify/deci/Deci.kt +++ b/deci/src/wasmJsMain/kotlin/org/kimplify/deci/Deci.kt @@ -77,14 +77,8 @@ actual class Deci private constructor( } actual override fun toString(): String { - val scale = _scale ?: return internal.toString() - if (scale <= 0) return internal.toString() - return internal.toFixed(scale) - } - - actual fun toPlainString(): String { val scale = _scale - return if (scale != null) internal.toFixed(scale) else internal.toFixed() + return if (scale != null && scale > 0) internal.toFixed(scale) else internal.toFixed() } actual fun toDouble(): Double = internal.toNumber() From abad93e28ef01a061d8e2185638fc52a31fbbb84 Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 13:05:49 +1100 Subject: [PATCH 10/19] =?UTF-8?q?Task=206:=20Apple=20actual=20=E2=80=94=20?= =?UTF-8?q?remove=20toPlainString();=20toString=20already=20handles=20plai?= =?UTF-8?q?n-string=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- deci/src/appleMain/kotlin/org/kimplify/deci/Deci.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/deci/src/appleMain/kotlin/org/kimplify/deci/Deci.kt b/deci/src/appleMain/kotlin/org/kimplify/deci/Deci.kt index a867b64..72ded4e 100644 --- a/deci/src/appleMain/kotlin/org/kimplify/deci/Deci.kt +++ b/deci/src/appleMain/kotlin/org/kimplify/deci/Deci.kt @@ -171,16 +171,6 @@ actual class Deci private constructor( return if (currentScale >= scale) str else str + "0".repeat(scale - currentScale) } - actual fun toPlainString(): String { - val str = internal.stringValue - val scale = _scale ?: return str - if (scale == 0) return str.split(".")[0] - val parts = str.split(".") - val intPart = parts[0] - val fracPart = if (parts.size > 1) parts[1] else "" - return "$intPart.${fracPart.padEnd(scale, '0')}" - } - actual fun toDouble(): Double = internal.doubleValue actual fun isZero(): Boolean = internal.compare(NSDecimalNumber.zero) == 0L From c1d78e30b6df7954d91193ed321cfc1eae5cc85b Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 13:08:12 +1100 Subject: [PATCH 11/19] Migrate all internal toPlainString calls to toString Co-Authored-By: Claude Sonnet 4.6 --- .../kotlin/org/kimplify/deci/DeciSerializer.kt | 3 ++- .../org/kimplify/deci/extension/DeciExtensions.kt | 6 +++--- .../org/kimplify/deci/formatting/DeciFormatting.kt | 14 +++++++------- .../kotlin/org/kimplify/deci/math/DeciMath.kt | 2 +- .../org/kimplify/deci/validation/DeciValidation.kt | 8 ++++---- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/deci/src/commonMain/kotlin/org/kimplify/deci/DeciSerializer.kt b/deci/src/commonMain/kotlin/org/kimplify/deci/DeciSerializer.kt index ca1a9fa..1a36a5c 100644 --- a/deci/src/commonMain/kotlin/org/kimplify/deci/DeciSerializer.kt +++ b/deci/src/commonMain/kotlin/org/kimplify/deci/DeciSerializer.kt @@ -15,6 +15,7 @@ import org.kimplify.deci.exception.DeciSerializationException * For example, `Deci("1.50")` is serialized as the JSON string `"1.50"`, not the * JSON number `1.5`. * + * Serialization uses [Deci.toString], which never produces scientific notation. * Deserialization parses the decoded string via the [Deci] string constructor. * * @throws [org.kimplify.deci.exception.DeciSerializationException] if the decoded string @@ -27,7 +28,7 @@ object DeciSerializer : KSerializer { encoder: Encoder, value: Deci, ) { - encoder.encodeString(value.toPlainString()) + encoder.encodeString(value.toString()) } override fun deserialize(decoder: Decoder): Deci { diff --git a/deci/src/commonMain/kotlin/org/kimplify/deci/extension/DeciExtensions.kt b/deci/src/commonMain/kotlin/org/kimplify/deci/extension/DeciExtensions.kt index de95f32..fed9cc8 100644 --- a/deci/src/commonMain/kotlin/org/kimplify/deci/extension/DeciExtensions.kt +++ b/deci/src/commonMain/kotlin/org/kimplify/deci/extension/DeciExtensions.kt @@ -31,7 +31,7 @@ fun Deci.toLong(): Long = toLongExact() */ fun Deci.toLongOrNull(): Long? { val truncated = this.setScale(0, RoundingMode.DOWN) - val str = truncated.toPlainString() + val str = truncated.toString() return str.toLongOrNull() } @@ -53,7 +53,7 @@ fun Deci.toLongExact(): Long = * @return the scale (number of fractional digits), or `0` if there is no decimal separator. */ fun Deci.scale(): Int { - val text = toPlainString() + val text = toString() val separatorIndex = text.indexOf('.') if (separatorIndex < 0) return 0 return text.length - separatorIndex - 1 @@ -65,7 +65,7 @@ fun Deci.scale(): Int { * @return the total number of digit characters in the canonical string representation. */ fun Deci.precision(): Int { - val text = toPlainString() + val text = toString() return text.count { it.isDigit() } } diff --git a/deci/src/commonMain/kotlin/org/kimplify/deci/formatting/DeciFormatting.kt b/deci/src/commonMain/kotlin/org/kimplify/deci/formatting/DeciFormatting.kt index e8feb1d..4446f58 100644 --- a/deci/src/commonMain/kotlin/org/kimplify/deci/formatting/DeciFormatting.kt +++ b/deci/src/commonMain/kotlin/org/kimplify/deci/formatting/DeciFormatting.kt @@ -34,7 +34,7 @@ fun Deci.formatCurrency( * @return Formatted string with thousands separators */ fun Deci.formatWithThousandsSeparator(separator: String = ","): String { - val str = this.toPlainString() + val str = this.toString() val parts = str.split(".") val integerPart = parts[0] val decimalPart = if (parts.size > 1) ".${parts[1]}" else "" @@ -66,7 +66,7 @@ fun Deci.formatAsPercentage( ): String { val percentage = this * DeciConstants.HUNDRED val rounded = percentage.setScale(scale, RoundingMode.HALF_UP) - return "${rounded.toPlainString()}$symbol" + return "${rounded.toString()}$symbol" } /** @@ -88,7 +88,7 @@ fun Deci.formatAsPercentage( fun Deci.toScientificNotation(precision: Int = 6): String { if (this.isZero()) return "0.0E+0" - val str = this.abs().toPlainString() + val str = this.abs().toString() val allDigits = str.filter { it.isDigit() } val firstNonZeroIdx = allDigits.indexOfFirst { it != '0' } if (firstNonZeroIdx == -1) return "0.0E+0" @@ -138,9 +138,9 @@ fun Deci.toScientificNotation(precision: Int = 6): String { */ fun Deci.format(pattern: String): String = when (pattern) { - "0.00" -> this.setScale(2, RoundingMode.HALF_UP).toPlainString() + "0.00" -> this.setScale(2, RoundingMode.HALF_UP).toString() "#,##0.00" -> this.setScale(2, RoundingMode.HALF_UP).formatWithThousandsSeparator() - "0.0000" -> this.setScale(4, RoundingMode.HALF_UP).toPlainString() + "0.0000" -> this.setScale(4, RoundingMode.HALF_UP).toString() "#,##0" -> this.setScale(0, RoundingMode.HALF_UP).formatWithThousandsSeparator() else -> throw DeciFormatException(pattern = pattern) } @@ -156,7 +156,7 @@ fun Deci.toWords(): String { val isNegative = this.isNegative() val abs = this.abs() - val parts = abs.toPlainString().split(".") + val parts = abs.toString().split(".") val integerPart = parts[0].toLongOrNull() ?: return "number too large" val fractionalPart = @@ -254,7 +254,7 @@ fun Deci.pad( padChar: Char = ' ', padLeft: Boolean = true, ): String { - val str = this.toPlainString() + val str = this.toString() return if (padLeft) { str.padStart(width, padChar) } else { diff --git a/deci/src/commonMain/kotlin/org/kimplify/deci/math/DeciMath.kt b/deci/src/commonMain/kotlin/org/kimplify/deci/math/DeciMath.kt index 10db6ed..10efe4a 100644 --- a/deci/src/commonMain/kotlin/org/kimplify/deci/math/DeciMath.kt +++ b/deci/src/commonMain/kotlin/org/kimplify/deci/math/DeciMath.kt @@ -152,7 +152,7 @@ fun Deci.roundToSignificantDigits(digits: Int): Deci { if (this.isZero()) return Deci.ZERO val absValue = this.abs() - val str = absValue.toPlainString() + val str = absValue.toString() val firstSigDigitIndex = str.indexOfFirst { it.isDigit() && it != '0' } if (firstSigDigitIndex == -1) return Deci.ZERO diff --git a/deci/src/commonMain/kotlin/org/kimplify/deci/validation/DeciValidation.kt b/deci/src/commonMain/kotlin/org/kimplify/deci/validation/DeciValidation.kt index 449ec8f..5e6dea6 100644 --- a/deci/src/commonMain/kotlin/org/kimplify/deci/validation/DeciValidation.kt +++ b/deci/src/commonMain/kotlin/org/kimplify/deci/validation/DeciValidation.kt @@ -63,7 +63,7 @@ fun Deci.clamp( * @return True if this is a whole number */ fun Deci.isWhole(): Boolean { - val str = this.toPlainString() + val str = this.toString() return !str.contains('.') || str.endsWith(".0") || str.substringAfter('.').all { it == '0' } } @@ -115,7 +115,7 @@ fun Deci.safeDivide( fun Deci.hasValidDecimalPlaces(maxDecimalPlaces: Int): Boolean { require(maxDecimalPlaces >= 0) { "Max decimal places must be non-negative: $maxDecimalPlaces" } - val str = this.toPlainString() + val str = this.toString() val decimalIndex = str.indexOf('.') return if (decimalIndex == -1) { @@ -234,7 +234,7 @@ fun Deci.validateForForm( if (this < min) { return ValidationResult( false, - "Value must be at least ${min.toPlainString()}", + "Value must be at least ${min.toString()}", ) } } @@ -243,7 +243,7 @@ fun Deci.validateForForm( if (this > max) { return ValidationResult( false, - "Value must be at most ${max.toPlainString()}", + "Value must be at most ${max.toString()}", ) } } From c43462cd83051c59dc91b3059bce705ed28a2f2c Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 13:10:27 +1100 Subject: [PATCH 12/19] Update all tests: toPlainString -> toString, fix trailing zeros assertion Co-Authored-By: Claude Sonnet 4.6 --- .../org/kimplify/deci/DeciPropertyTest.kt | 2 +- .../kimplify/deci/DeciSerializationTest.kt | 2 +- .../kotlin/org/kimplify/deci/DeciTest.kt | 19 +++++++------------ .../deci/bulk/DeciBulkOperationsTest.kt | 2 +- .../deci/math/DeciMathExtendedTest.kt | 2 +- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/deci/src/commonTest/kotlin/org/kimplify/deci/DeciPropertyTest.kt b/deci/src/commonTest/kotlin/org/kimplify/deci/DeciPropertyTest.kt index f414a11..87a8ed9 100644 --- a/deci/src/commonTest/kotlin/org/kimplify/deci/DeciPropertyTest.kt +++ b/deci/src/commonTest/kotlin/org/kimplify/deci/DeciPropertyTest.kt @@ -204,7 +204,7 @@ class DeciPropertyTest { fun `compareTo zero implies equality`() = runTest { checkAll(config, arbDeci()) { a -> - val copy = Deci(a.toPlainString()) + val copy = Deci(a.toString()) assertTrue(a.compareTo(copy) == 0, "a=$a not equal to copy") assertEquals(a, copy) } diff --git a/deci/src/commonTest/kotlin/org/kimplify/deci/DeciSerializationTest.kt b/deci/src/commonTest/kotlin/org/kimplify/deci/DeciSerializationTest.kt index 6e5d47c..7cbc181 100644 --- a/deci/src/commonTest/kotlin/org/kimplify/deci/DeciSerializationTest.kt +++ b/deci/src/commonTest/kotlin/org/kimplify/deci/DeciSerializationTest.kt @@ -80,7 +80,7 @@ class DeciSerializationTest { val encoded = json.encodeToString(Deci.serializer(), d) val restored = json.decodeFromString(Deci.serializer(), encoded) assertEquals(d, restored, "Round-trip failed for $s") - assertEquals(s, restored.toPlainString(), "String representation changed for $s") + assertEquals(s, restored.toString(), "String representation changed for $s") } } diff --git a/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt b/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt index 7607607..a2bb206 100644 --- a/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt +++ b/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt @@ -255,10 +255,10 @@ class DeciTest { assertEquals(Deci("10"), Deci.TEN) } - @Test fun `toDouble and toPlainString roundtrip for simple values`() { + @Test fun `toDouble and toString roundtrip for simple values`() { listOf("0", "1.5", "-2.75").forEach { s -> val d = Deci(s) - assertEquals(s, d.toPlainString()) + assertEquals(s, d.toString()) assertEquals(s.toDouble(), d.toDouble()) } } @@ -282,18 +282,13 @@ class DeciTest { assertEquals(Deci.ZERO, Deci("-0")) } - @Test fun `trailing zeros are stripped by constructor`() { - assertEquals("1.23", Deci("1.2300").toPlainString()) + @Test fun `trailing zeros are preserved by constructor`() { + assertEquals("1.2300", Deci("1.2300").toString()) } - @Test fun `toPlainString never uses scientific notation`() { - assertEquals("100000000000000000000", Deci("100000000000000000000").toPlainString()) - assertEquals("0.000000001", Deci("0.000000001").toPlainString()) - } - - @Test fun `toString may use scientific notation for extreme values`() { - val large = Deci("100000000000000000000") - assertEquals(large, Deci(large.toPlainString())) + @Test fun `toString never uses scientific notation`() { + assertEquals("100000000000000000000", Deci("100000000000000000000").toString()) + assertEquals("0.000000001", Deci("0.000000001").toString()) } @Test fun `invalid string formats throw`() { diff --git a/deci/src/commonTest/kotlin/org/kimplify/deci/bulk/DeciBulkOperationsTest.kt b/deci/src/commonTest/kotlin/org/kimplify/deci/bulk/DeciBulkOperationsTest.kt index 25ecb2b..23ea7b3 100644 --- a/deci/src/commonTest/kotlin/org/kimplify/deci/bulk/DeciBulkOperationsTest.kt +++ b/deci/src/commonTest/kotlin/org/kimplify/deci/bulk/DeciBulkOperationsTest.kt @@ -264,7 +264,7 @@ class DeciBulkOperationsTest { @Test fun `divideAllBy uses default context when not specified`() { val result = listOf(Deci("10")).divideAllBy(Deci("3")) - assertTrue(result[0].toPlainString().length > 5) + assertTrue(result[0].toString().length > 5) } @Test diff --git a/deci/src/commonTest/kotlin/org/kimplify/deci/math/DeciMathExtendedTest.kt b/deci/src/commonTest/kotlin/org/kimplify/deci/math/DeciMathExtendedTest.kt index c9ffa97..ade4f2c 100644 --- a/deci/src/commonTest/kotlin/org/kimplify/deci/math/DeciMathExtendedTest.kt +++ b/deci/src/commonTest/kotlin/org/kimplify/deci/math/DeciMathExtendedTest.kt @@ -265,7 +265,7 @@ class DeciMathExtendedTest { @Test fun `roundToSignificantDigits preserves more digits than available`() { val result = Deci("12").roundToSignificantDigits(5) - assertTrue(result.toPlainString().startsWith("12")) + assertTrue(result.toString().startsWith("12")) } @Test From 1764a102f011283769e0477d8e68ab457200dd5a Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 13:10:31 +1100 Subject: [PATCH 13/19] Sample app: toPlainString -> toString Co-Authored-By: Claude Sonnet 4.6 --- .../commonMain/kotlin/org/kimplify/screens/ValidationScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample/composeApp/src/commonMain/kotlin/org/kimplify/screens/ValidationScreen.kt b/sample/composeApp/src/commonMain/kotlin/org/kimplify/screens/ValidationScreen.kt index d60c391..96eff62 100644 --- a/sample/composeApp/src/commonMain/kotlin/org/kimplify/screens/ValidationScreen.kt +++ b/sample/composeApp/src/commonMain/kotlin/org/kimplify/screens/ValidationScreen.kt @@ -281,7 +281,7 @@ private fun SerializationSection() { DemoItem("Deserialized: $deserialized") DemoItem( "Trailing zeros preserved: ${ - original.toPlainString() == deserialized.toPlainString() + original.toString() == deserialized.toString() }", ) From be5f246769208fa1559f219922e7953b4a29a755 Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 13:26:10 +1100 Subject: [PATCH 14/19] Fix ktlint violations, update division test for preserved trailing zeros - Remove consecutive blank line in Deci.kt expect declaration - Remove redundant toString() calls in string templates (DeciFormatting, DeciValidation) - Update division test to expect full 20-digit scale (consequence of preserving trailing zeros) - Update API dump (toPlainString removed from public API) Co-Authored-By: Claude Opus 4.6 (1M context) --- deci/api/jvm/deci.api | 1 - deci/src/commonMain/kotlin/org/kimplify/deci/Deci.kt | 1 - .../kotlin/org/kimplify/deci/formatting/DeciFormatting.kt | 2 +- .../kotlin/org/kimplify/deci/validation/DeciValidation.kt | 4 ++-- deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/deci/api/jvm/deci.api b/deci/api/jvm/deci.api index ad5797a..d1bd540 100644 --- a/deci/api/jvm/deci.api +++ b/deci/api/jvm/deci.api @@ -25,7 +25,6 @@ public final class org/kimplify/deci/Deci : java/lang/Comparable { public final fun setScale (ILorg/kimplify/deci/RoundingMode;)Lorg/kimplify/deci/Deci; public final fun times (Lorg/kimplify/deci/Deci;)Lorg/kimplify/deci/Deci; public final fun toDouble ()D - public final fun toPlainString ()Ljava/lang/String; public fun toString ()Ljava/lang/String; public final fun unaryMinus ()Lorg/kimplify/deci/Deci; } diff --git a/deci/src/commonMain/kotlin/org/kimplify/deci/Deci.kt b/deci/src/commonMain/kotlin/org/kimplify/deci/Deci.kt index dcdb691..d7ee8d4 100644 --- a/deci/src/commonMain/kotlin/org/kimplify/deci/Deci.kt +++ b/deci/src/commonMain/kotlin/org/kimplify/deci/Deci.kt @@ -129,7 +129,6 @@ expect class Deci : Comparable { roundingMode: RoundingMode, ): Deci - /** * Returns the string representation of this [Deci] without scientific notation, * preserving the scale (e.g. `"1.50"` stays `"1.50"`). diff --git a/deci/src/commonMain/kotlin/org/kimplify/deci/formatting/DeciFormatting.kt b/deci/src/commonMain/kotlin/org/kimplify/deci/formatting/DeciFormatting.kt index 4446f58..d24cccd 100644 --- a/deci/src/commonMain/kotlin/org/kimplify/deci/formatting/DeciFormatting.kt +++ b/deci/src/commonMain/kotlin/org/kimplify/deci/formatting/DeciFormatting.kt @@ -66,7 +66,7 @@ fun Deci.formatAsPercentage( ): String { val percentage = this * DeciConstants.HUNDRED val rounded = percentage.setScale(scale, RoundingMode.HALF_UP) - return "${rounded.toString()}$symbol" + return "$rounded$symbol" } /** diff --git a/deci/src/commonMain/kotlin/org/kimplify/deci/validation/DeciValidation.kt b/deci/src/commonMain/kotlin/org/kimplify/deci/validation/DeciValidation.kt index 5e6dea6..9a3fcbd 100644 --- a/deci/src/commonMain/kotlin/org/kimplify/deci/validation/DeciValidation.kt +++ b/deci/src/commonMain/kotlin/org/kimplify/deci/validation/DeciValidation.kt @@ -234,7 +234,7 @@ fun Deci.validateForForm( if (this < min) { return ValidationResult( false, - "Value must be at least ${min.toString()}", + "Value must be at least $min", ) } } @@ -243,7 +243,7 @@ fun Deci.validateForForm( if (this > max) { return ValidationResult( false, - "Value must be at most ${max.toString()}", + "Value must be at most $max", ) } } diff --git a/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt b/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt index a2bb206..c736bb9 100644 --- a/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt +++ b/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt @@ -63,7 +63,7 @@ class DeciTest { @Test fun `div should divide with default scale`() { val result = Deci("5") / Deci("2") - assertEquals("2.5", result.toString()) + assertEquals("2.50000000000000000000", result.toString()) } @Test From 6c9e27014250328adbadfdd91ac9f89b42bdfd8b Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 13:30:22 +1100 Subject: [PATCH 15/19] Fix Android API dump and update CHANGELOG for breaking change - Remove toPlainString from Android API dump (was missed by apiDump task) - Add [Unreleased] changelog entry documenting the removal of toPlainString and the trailing zeros preservation as breaking changes with migration guide Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ deci/api/android/deci.api | 1 - 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e4cb5c..b9a36c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## [Unreleased] + +### Breaking Changes + +- **`toPlainString()` removed** — `toString()` is now the single string representation + method. It never uses scientific notation and preserves trailing zeros (e.g. + `Deci("1.50").toString()` returns `"1.50"`). This reverts the 0.2.0 change that + introduced scientific notation in `toString()`. + +- **Trailing zeros are now preserved** — `Deci("1.2300").toString()` returns `"1.2300"`, + not `"1.23"`. The constructor no longer strips trailing zeros on JVM/Android. + +### Migration Guide + +If you adopted `toPlainString()` from 0.2.0, simply revert to `toString()`: + +```kotlin +// 0.2.0 +val text = myDeci.toPlainString() + +// Now +val text = myDeci.toString() +``` + +`toString()` now behaves like the old `toPlainString()` — plain decimal, no scientific +notation, scale preserved. String interpolation (`"$deci"`) also works as expected. + +**Serialization is unaffected** — `DeciSerializer` continues to produce plain decimal +strings in JSON. + ## [0.2.1] - 2026-03-24 ### Fixed diff --git a/deci/api/android/deci.api b/deci/api/android/deci.api index ad5797a..d1bd540 100644 --- a/deci/api/android/deci.api +++ b/deci/api/android/deci.api @@ -25,7 +25,6 @@ public final class org/kimplify/deci/Deci : java/lang/Comparable { public final fun setScale (ILorg/kimplify/deci/RoundingMode;)Lorg/kimplify/deci/Deci; public final fun times (Lorg/kimplify/deci/Deci;)Lorg/kimplify/deci/Deci; public final fun toDouble ()D - public final fun toPlainString ()Ljava/lang/String; public fun toString ()Ljava/lang/String; public final fun unaryMinus ()Lorg/kimplify/deci/Deci; } From 7259ae61bb05cd193596503752a888ec2d5a7509 Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 13:45:57 +1100 Subject: [PATCH 16/19] Clean up CHANGELOG: 0.2.0 was never published to Maven Remove the toPlainString addition and migration guide from 0.2.0 since it was never released. Move toString improvements to 0.2.1 as regular changes rather than breaking changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 70 ++++++---------------------------------------------- 1 file changed, 8 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9a36c4..36e5a77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,35 +1,5 @@ # Changelog -## [Unreleased] - -### Breaking Changes - -- **`toPlainString()` removed** — `toString()` is now the single string representation - method. It never uses scientific notation and preserves trailing zeros (e.g. - `Deci("1.50").toString()` returns `"1.50"`). This reverts the 0.2.0 change that - introduced scientific notation in `toString()`. - -- **Trailing zeros are now preserved** — `Deci("1.2300").toString()` returns `"1.2300"`, - not `"1.23"`. The constructor no longer strips trailing zeros on JVM/Android. - -### Migration Guide - -If you adopted `toPlainString()` from 0.2.0, simply revert to `toString()`: - -```kotlin -// 0.2.0 -val text = myDeci.toPlainString() - -// Now -val text = myDeci.toString() -``` - -`toString()` now behaves like the old `toPlainString()` — plain decimal, no scientific -notation, scale preserved. String interpolation (`"$deci"`) also works as expected. - -**Serialization is unaffected** — `DeciSerializer` continues to produce plain decimal -strings in JSON. - ## [0.2.1] - 2026-03-24 ### Fixed @@ -45,43 +15,19 @@ strings in JSON. - **Sample app restructured** into a 5-tab navigation app (Core, Scale & Context, Financial, Format & Stats, Validation) showcasing all library features. -## [0.2.0] - 2026-03-20 +- **`toString()` now preserves trailing zeros** — `Deci("1.50").toString()` returns + `"1.50"`, and `Deci("1.2300").toString()` returns `"1.2300"`. The JVM/Android + constructor no longer strips trailing zeros. -### Breaking Changes +- **`toString()` never uses scientific notation** — guaranteed across all platforms + (JVM, Android, JS, wasmJs, Apple). For example, `Deci("100000000000000000000").toString()` + always returns `"100000000000000000000"`, never `"1E+20"`. -- **`toString()` may now return scientific notation** for very large or very small values. - On JVM/Android this delegates to `BigDecimal.toString()`, on JS/WASM to `decimal.js`'s `toString()`. - Previously, `toString()` always returned a plain decimal string. +## [0.2.0] - 2026-03-20 ### Added -- **`toPlainString()`** — returns the string representation without scientific notation, - preserving the scale (e.g. `"1.50"` stays `"1.50"`). This is the behaviour that - `toString()` had in 0.1.x. - -### Migration Guide - -Replace `toString()` with `toPlainString()` wherever you need a plain decimal string: - -```kotlin -// Before (0.1.x) -val text = myDeci.toString() // always "100000000000000000000" - -// After (0.2.0) -val text = myDeci.toPlainString() // always "100000000000000000000" -val debug = myDeci.toString() // may return "1E+20" on JVM -``` - -Common patterns to update: - -| Old pattern | New pattern | -|---|---| -| `deci.toString()` (for display/storage) | `deci.toPlainString()` | -| `Deci(someDeci.toString())` | `Deci(someDeci.toPlainString())` | -| `"Amount: $deci"` (if plain format needed) | `"Amount: ${deci.toPlainString()}"` | - -**Serialization is unaffected** — the built-in `DeciSerializer` has been updated internally -to use `toPlainString()`, so JSON output remains a plain decimal string. +- Comprehensive sample app with tabbed navigation ## [0.1.1] From d5349bd33b18a7910846603a10a99bb58240e661 Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 13:51:48 +1100 Subject: [PATCH 17/19] Fix tests for cross-platform consistency - Division test: assert value equality instead of string representation, since scale propagation in toString() varies by platform - Trailing zeros test: assert numeric equivalence and round-trip instead of exact string form, since scale tracking differs across platforms Co-Authored-By: Claude Opus 4.6 (1M context) --- deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt b/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt index c736bb9..53f8b9e 100644 --- a/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt +++ b/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt @@ -63,7 +63,7 @@ class DeciTest { @Test fun `div should divide with default scale`() { val result = Deci("5") / Deci("2") - assertEquals("2.50000000000000000000", result.toString()) + assertEquals(Deci("2.5"), result) } @Test @@ -283,7 +283,10 @@ class DeciTest { } @Test fun `trailing zeros are preserved by constructor`() { - assertEquals("1.2300", Deci("1.2300").toString()) + val fromLiteral = Deci("1.2300") + assertEquals(Deci("1.23"), fromLiteral) + val roundTripped = Deci(fromLiteral.toString()) + assertEquals(fromLiteral, roundTripped) } @Test fun `toString never uses scientific notation`() { From 54836e21b0f41fcdef395e4809eb659e9549ce1d Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 13:56:52 +1100 Subject: [PATCH 18/19] Track scale from construction and division on JS/wasmJs/Apple - Add extractScale() utility to parse scale from normalized decimal strings - JS/wasmJs/Apple string constructors now store _scale from input - JS/wasmJs/Apple / operator now passes policy.fractionalDigits as _scale - Restore strong cross-platform test assertions for trailing zeros and division scale in toString() This ensures Deci("1.50").toString() returns "1.50" and division results include the full policy scale on all platforms, not just JVM/Android. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/appleMain/kotlin/org/kimplify/deci/Deci.kt | 4 +++- .../kotlin/org/kimplify/deci/parser/StringUtils.kt | 14 ++++++++++++++ .../kotlin/org/kimplify/deci/DeciTest.kt | 7 ++----- deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt | 4 +++- .../wasmJsMain/kotlin/org/kimplify/deci/Deci.kt | 4 +++- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/deci/src/appleMain/kotlin/org/kimplify/deci/Deci.kt b/deci/src/appleMain/kotlin/org/kimplify/deci/Deci.kt index 72ded4e..647d117 100644 --- a/deci/src/appleMain/kotlin/org/kimplify/deci/Deci.kt +++ b/deci/src/appleMain/kotlin/org/kimplify/deci/Deci.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable import org.kimplify.deci.config.DeciConfiguration import org.kimplify.deci.exception.DeciDivisionByZeroException import org.kimplify.deci.exception.DeciScaleException +import org.kimplify.deci.parser.extractScale import org.kimplify.deci.parser.validateAndNormalizeDecimalLiteral import platform.Foundation.NSDecimalNumber import platform.Foundation.NSDecimalNumberHandler @@ -19,6 +20,7 @@ actual class Deci private constructor( ) : Comparable { actual constructor(value: String) : this( NSDecimalNumber(validateAndNormalizeDecimalLiteral(value)), + extractScale(validateAndNormalizeDecimalLiteral(value)), ) actual constructor(value: Long) : this(value.toString()) @@ -119,7 +121,7 @@ actual class Deci private constructor( raiseOnUnderflow = false, raiseOnDivideByZero = false, ) - return Deci(raw.decimalNumberByRoundingAccordingToBehavior(handler).stringValue) + return Deci(raw.decimalNumberByRoundingAccordingToBehavior(handler), policy.fractionalDigits) } actual fun divide( diff --git a/deci/src/commonMain/kotlin/org/kimplify/deci/parser/StringUtils.kt b/deci/src/commonMain/kotlin/org/kimplify/deci/parser/StringUtils.kt index 9997739..9992b9e 100644 --- a/deci/src/commonMain/kotlin/org/kimplify/deci/parser/StringUtils.kt +++ b/deci/src/commonMain/kotlin/org/kimplify/deci/parser/StringUtils.kt @@ -78,3 +78,17 @@ private fun normalizeWithDecimal( return "$safeInteger.$decimalPart" } + +/** + * Extracts the scale (number of digits after the decimal point) from a normalized decimal string. + * + * Examples: + * "1.2300" -> 4 + * "123" -> 0 + * "-2.5" -> 1 + * "0.5" -> 1 + */ +internal fun extractScale(normalizedValue: String): Int { + val dotIndex = normalizedValue.indexOf('.') + return if (dotIndex < 0) 0 else normalizedValue.length - dotIndex - 1 +} diff --git a/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt b/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt index 53f8b9e..c736bb9 100644 --- a/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt +++ b/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt @@ -63,7 +63,7 @@ class DeciTest { @Test fun `div should divide with default scale`() { val result = Deci("5") / Deci("2") - assertEquals(Deci("2.5"), result) + assertEquals("2.50000000000000000000", result.toString()) } @Test @@ -283,10 +283,7 @@ class DeciTest { } @Test fun `trailing zeros are preserved by constructor`() { - val fromLiteral = Deci("1.2300") - assertEquals(Deci("1.23"), fromLiteral) - val roundTripped = Deci(fromLiteral.toString()) - assertEquals(fromLiteral, roundTripped) + assertEquals("1.2300", Deci("1.2300").toString()) } @Test fun `toString never uses scientific notation`() { diff --git a/deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt b/deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt index ca3f905..6af7e05 100644 --- a/deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt +++ b/deci/src/jsMain/kotlin/org/kimplify/deci/Deci.kt @@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable import org.kimplify.deci.config.DeciConfiguration import org.kimplify.deci.exception.DeciDivisionByZeroException import org.kimplify.deci.exception.DeciScaleException +import org.kimplify.deci.parser.extractScale import org.kimplify.deci.parser.validateAndNormalizeDecimalLiteral @Serializable(with = DeciSerializer::class) @@ -13,6 +14,7 @@ actual class Deci private constructor( ) : Comparable { actual constructor(value: String) : this( DecimalJs(validateAndNormalizeDecimalLiteral(value)), + extractScale(validateAndNormalizeDecimalLiteral(value)), ) actual constructor(value: Long) : this(value.toString()) @@ -42,7 +44,7 @@ actual class Deci private constructor( val policy = DeciConfiguration.divisionPolicy val raw = internal.div(other.internal) val rounded = raw.toDecimalPlaces(policy.fractionalDigits, convert(policy.roundingMode)) - return Deci(rounded) + return Deci(rounded, policy.fractionalDigits) } actual operator fun rem(other: Deci): Deci { diff --git a/deci/src/wasmJsMain/kotlin/org/kimplify/deci/Deci.kt b/deci/src/wasmJsMain/kotlin/org/kimplify/deci/Deci.kt index ca3f905..6af7e05 100644 --- a/deci/src/wasmJsMain/kotlin/org/kimplify/deci/Deci.kt +++ b/deci/src/wasmJsMain/kotlin/org/kimplify/deci/Deci.kt @@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable import org.kimplify.deci.config.DeciConfiguration import org.kimplify.deci.exception.DeciDivisionByZeroException import org.kimplify.deci.exception.DeciScaleException +import org.kimplify.deci.parser.extractScale import org.kimplify.deci.parser.validateAndNormalizeDecimalLiteral @Serializable(with = DeciSerializer::class) @@ -13,6 +14,7 @@ actual class Deci private constructor( ) : Comparable { actual constructor(value: String) : this( DecimalJs(validateAndNormalizeDecimalLiteral(value)), + extractScale(validateAndNormalizeDecimalLiteral(value)), ) actual constructor(value: Long) : this(value.toString()) @@ -42,7 +44,7 @@ actual class Deci private constructor( val policy = DeciConfiguration.divisionPolicy val raw = internal.div(other.internal) val rounded = raw.toDecimalPlaces(policy.fractionalDigits, convert(policy.roundingMode)) - return Deci(rounded) + return Deci(rounded, policy.fractionalDigits) } actual operator fun rem(other: Deci): Deci { From 0fdffee474a2d25538d5ebe4fc0358c18240b23e Mon Sep 17 00:00:00 2001 From: Merkost Date: Fri, 27 Mar 2026 14:09:35 +1100 Subject: [PATCH 19/19] Add tests for small values in toString and toScientificNotation methods --- deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt b/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt index c736bb9..fed1834 100644 --- a/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt +++ b/deci/src/commonTest/kotlin/org/kimplify/deci/DeciTest.kt @@ -8,6 +8,7 @@ import org.kimplify.deci.exception.DeciParseException import org.kimplify.deci.exception.DeciScaleException import org.kimplify.deci.extension.precision import org.kimplify.deci.extension.scale +import org.kimplify.deci.formatting.toScientificNotation import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -289,6 +290,13 @@ class DeciTest { @Test fun `toString never uses scientific notation`() { assertEquals("100000000000000000000", Deci("100000000000000000000").toString()) assertEquals("0.000000001", Deci("0.000000001").toString()) + assertEquals("0.00000000000001", Deci("0.00000000000001").toString()) + } + + @Test fun `toString and toScientificNotation for very small values`() { + val small = Deci("0.00000000000001") + assertEquals("0.00000000000001", small.toString()) + assertEquals("1E-14", small.toScientificNotation()) } @Test fun `invalid string formats throw`() {