Skip to content

fix(core): JPY (and other) currency fraction digits follow the currency, not the locale#11

Merged
Merkost merged 8 commits into
mainfrom
fix/jpy-fraction-digits-issue-10
Jun 5, 2026
Merged

fix(core): JPY (and other) currency fraction digits follow the currency, not the locale#11
Merkost merged 8 commits into
mainfrom
fix/jpy-fraction-digits-issue-10

Conversation

@Merkost

@Merkost Merkost commented Jun 4, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #10. Formatting JPY under a non-Japanese locale produced ¥1,235.00 instead of ¥1,235, because the formatter inherited the fraction-digit count from the display locale's default currency rather than the currency being formatted.

While fixing this, kurrency-core was decoupled from Compose (the real reason the Android tests couldn't build/run), and the Android instrumented test suite was repaired so the fix is verified on a real device.

Root cause

NumberFormat.getCurrencyInstance(locale) pre-configures the formatter for the locale's default currency — both symbol and fraction-digit count. java.text's setCurrency() then swaps the symbol but, by contract, does not update the digit count. So a US-locale formatter kept 2 digits even for JPY. It only worked for Locale.JAPAN by coincidence (its default currency is JPY).

The fix

Platform Change
Android Use ICU's NumberFormat, whose setCurrency() updates the fraction digits automatically (consistent with the compact formatter, already on ICU).
JVM Pin min/maxFractionDigits to the currency's defaultFractionDigits.
JS / WasmJs / iOS No change — already derived digits from the currency.

Supporting changes

  • Decouple Compose from coreCurrencyState (the only Compose user in core) moved to kurrency-compose, and the Compose plugins + compose.runtime were removed from kurrency-core, restoring it as a dependency-free foundation. This also removed the broken Compose Gradle task that blocked Android tests.
  • Repair Android instrumented tests — they lived in src/androidTest (ignored by the androidLibrary plugin) with deps on the wrong source set, so connectedAndroidDeviceTest ran 0 tests. Moved to src/androidDeviceTest and wired correctly.
  • isValidCurrency on AndroidCurrency.getInstance (ICU) accepts any well-formed 3-letter code, so isValid("XYZ") wrongly returned true; now validated against the known ISO 4217 set, matching JVM/iOS.

Testing

  • Added a common regression test plus an Android instrumented one for the JPY case.
  • kurrency-core: JVM (404), JS, WasmJs, iOS — all pass.
  • kurrency-compose: JVM passes (now owns CurrencyState).
  • Android instrumented: 85 tests, 0 failures.

Merkost and others added 5 commits June 5, 2026 09:01
…dependency

CurrencyState was the only Compose user in kurrency-core, dragging
compose.runtime and the Compose Gradle plugins into what is meant to be the
dependency-free foundation module. It is intrinsically a Compose UI state
holder (mutableStateOf/derivedStateOf/@composable), so it now lives in
kurrency-compose, which already depends on core.

- Move CurrencyState, FormattedAmountDelegate, rememberCurrencyState and
  their test to org.kimplify.kurrency.compose
- Remove composeMultiplatform/composeCompiler plugins and compose.runtime
  from kurrency-core
- Drop the debug-log calls that relied on core-internal KurrencyLog
- Update sample imports to the new package

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ce set

The instrumented tests lived in src/androidTest with deps on the
androidInstrumentedTest source set, but the androidLibrary plugin's device-test
compilation uses androidDeviceTest (src/androidDeviceTest). As a result
connectedAndroidDeviceTest compiled and ran zero tests.

- Move instrumented tests to src/androidDeviceTest
- Point test deps at androidDeviceTest via getByName (no accessor is generated)
- Drop the AndroidX Test Orchestrator execution mode; its test-services APK was
  never provided, so runs failed with "No test results"
- Remove CurrencyStateTestInstrumented (CurrencyState moved to kurrency-compose;
  it is covered by its commonTest)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NumberFormat.getCurrencyInstance(locale) seeds the fraction-digit count from
the locale's default currency, and java.text's setCurrency() does not update
it. So formatting JPY (0 digits) under a non-JP locale produced "¥1,235.00"
instead of "¥1,235" (issue #10).

- JVM: pin min/max fraction digits to the currency's defaultFractionDigits
- Android: switch to ICU's NumberFormat, whose setCurrency() updates the
  fraction digits (and matches the compact formatter, already on ICU)
- Add a common regression test plus an Android instrumented one

JS/WasmJs (Intl.NumberFormat) and iOS (NSNumberFormatter) already derived the
digits from the currency and were unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Android's Currency.getInstance (ICU) returns an instance for any well-formed
3-letter code, so isValid("XYZ") wrongly returned true. Validate against the
set of known ISO 4217 currencies instead, matching JVM/iOS behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 4, 2026 23:51
@Merkost Merkost changed the base branch from develop to main June 4, 2026 23:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes incorrect fraction-digit handling when formatting currencies like JPY under non-native locales (issue #10), and includes supporting refactors to decouple kurrency-core from Compose plus repairs to Android instrumented/device testing so the regression is verified on-device.

Changes:

  • JVM: pin NumberFormat min/max fraction digits to the formatted currency’s defaultFractionDigits.
  • Android: switch currency formatting to ICU NumberFormat (so setCurrency() updates fraction digits) and tighten isValidCurrency to ISO-4217 codes.
  • Project structure/testing: move CurrencyState into kurrency-compose, adjust sample imports, and migrate instrumented tests to androidDeviceTest with updated Gradle wiring.

Reviewed changes

Copilot reviewed 12 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
sample/shared/src/commonMain/kotlin/org/kimplify/kurrency/sample/KurrencySample.kt Updates import path for rememberCurrencyState after moving Compose state APIs.
sample/shared/src/commonMain/kotlin/org/kimplify/kurrency/sample/CurrencyListScreen.kt Updates import path for rememberCurrencyState after moving Compose state APIs.
kurrency-core/src/jvmMain/kotlin/org/kimplify/kurrency/CurrencyFormatterImpl.kt Forces JVM currency formatter fraction digits to follow the currency (fixes JPY decimals under US locale).
kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/JpyFractionDigitsTest.kt Adds a common regression test ensuring JPY formatting has no decimals under non-JP locale.
kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyStateTestInstrumented.kt Removes Compose-dependent Android instrumented tests from core (state moved to compose module).
kurrency-core/src/androidMain/kotlin/org/kimplify/kurrency/CurrencyFormatterImpl.kt Switches Android formatting to ICU NumberFormat and changes Android isValidCurrency implementation.
kurrency-core/src/androidDeviceTest/kotlin/org/kimplify/kurrency/LocaleFormattingTestInstrumented.kt Adds Android device tests for formatting behavior across locales (needs minor test logic fixes).
kurrency-core/src/androidDeviceTest/kotlin/org/kimplify/kurrency/KurrencyLocaleTestInstrumented.kt Adds Android device tests for locale parsing and predefined locales.
kurrency-core/src/androidDeviceTest/kotlin/org/kimplify/kurrency/KurrencyErrorTestInstrumented.kt Adds Android device tests for error types/messages and Result propagation.
kurrency-core/src/androidDeviceTest/kotlin/org/kimplify/kurrency/JpyFractionDigitsTestInstrumented.kt Adds Android on-device regression test for JPY decimal handling.
kurrency-core/src/androidDeviceTest/kotlin/org/kimplify/kurrency/CurrencyTestInstrumented.kt Adds broad Android on-device tests for formatting, validity checks, and fraction digits.
kurrency-core/src/androidDeviceTest/kotlin/org/kimplify/kurrency/CurrencyMetadataTestInstrumented.kt Adds Android on-device tests for CurrencyMetadata parsing and data integrity.
kurrency-core/build.gradle.kts Removes Compose deps from core, switches Android target configuration, and wires androidDeviceTest deps.
kurrency-compose/src/commonTest/kotlin/org/kimplify/kurrency/compose/CurrencyStateTest.kt Updates tests to new org.kimplify.kurrency.compose package and adds missing imports.
kurrency-compose/src/commonMain/kotlin/org/kimplify/kurrency/compose/CurrencyState.kt Moves CurrencyState into compose module and removes logging calls.
gradle/wrapper/gradle-wrapper.properties Updates Gradle wrapper version.
gradle/libs.versions.toml Updates Android SDK/tooling and library versions (AGP/Kotlin/Compose/etc.).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Merkost and others added 3 commits June 5, 2026 10:16
Addresses PR review feedback:
- createNumberFormat (JVM + Android) uppercased the code only in validation,
  not when formatting, so lowercase codes (e.g. "usd", which isValid accepts)
  could throw and silently fall back to the raw amount. Uppercase before the
  Currency.getInstance lookup. Kotlin's uppercase() is locale-independent.
- isValidCurrency (Android) now checks a cached Set of ISO 4217 codes instead
  of linearly scanning getAvailableCurrencies() on every call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kotlinStdlib lagged at 2.3.21 while kotlin moved to 2.4.0, which diverged the
JS dependency resolution from the committed yarn.lock and failed CI's
kotlinStoreYarnLock check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The committed lock was from the Kotlin 2.3.20 toolchain; after the version
bump CI's kotlinStoreYarnLock failed with "Lock file was changed". Regenerated
via kotlinUpgradeYarnLock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Merkost Merkost self-assigned this Jun 5, 2026
@Merkost Merkost added the bug Something isn't working label Jun 5, 2026
@Merkost Merkost requested a review from diogocavaiar June 5, 2026 01:18
@Merkost Merkost merged commit 3e3751d into main Jun 5, 2026
2 checks passed
@Merkost Merkost deleted the fix/jpy-fraction-digits-issue-10 branch June 5, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Japanese Yen showing decimal places when the locale is not set to Japan

2 participants