Exclude withdrawn currencies from the picker, keep precious metals

Currency.getAvailableCurrencies() (JVM/Android) and NSLocale.ISOCurrencyCodes
(iOS/macOS) return every ISO 4217 code the platform has ever known about —
withdrawn currencies like the Deutsche Mark and French Franc, and test/
placeholder codes (XTS, XXX), included. Measured on the JVM: 232 total
codes, only 156 still assigned to a country.

Filters the list down to currencies actually assigned to an ISO country
today, which drops the withdrawn ones without needing a maintained
exclusion list. Precious metals (XAU, XAG, XPD, XPT) are added back in
since they're still actively traded despite not being tied to any country.
Web is left unfiltered — there's no reliable country-to-currency API there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpjMKWGoiT5aJBzhxvXkwp
This commit is contained in:
2026-09-08 17:20:09 +03:00
co-authored by Claude Sonnet 5
parent b0f3744851
commit 323ca43472
5 changed files with 89 additions and 0 deletions
@@ -5,9 +5,27 @@ import java.util.Locale
actual fun systemCurrencies(): List<CurrencyInfo> =
Currency.getAvailableCurrencies()
.filter { it.currencyCode in currentlyUsedCurrencyCodes() }
.map { CurrencyInfo(it.currencyCode, it.getDisplayName(Locale.getDefault())) }
.sortedBy { it.code }
/**
* `Currency.getAvailableCurrencies()` includes every ISO 4217 code the JDK has ever known about —
* historical currencies (Deutsche Mark, French Franc, ...) and test/placeholder codes (XTS, XXX)
* included. The currency currently assigned to each ISO country is a reliable proxy for "still in
* use" without needing a maintained exclusion list; precious metals are added back in since
* they're actively traded but aren't tied to any country.
*/
@Suppress("DEPRECATION") // Locale(language, country) still works fine; Locale.of() needs newer Android API levels.
private fun currentlyUsedCurrencyCodes(): Set<String> =
Locale.getISOCountries().mapNotNullTo(mutableSetOf()) { country ->
try {
Currency.getInstance(Locale("", country))?.currencyCode
} catch (e: IllegalArgumentException) {
null
}
} + PRECIOUS_METAL_CURRENCY_CODES
actual fun localeCurrencyCode(): String? = try {
Currency.getInstance(Locale.getDefault())?.currencyCode
} catch (e: IllegalArgumentException) {
@@ -5,11 +5,27 @@ import platform.Foundation.*
actual fun systemCurrencies(): List<CurrencyInfo> {
@Suppress("UNCHECKED_CAST")
val codes = NSLocale.Companion.ISOCurrencyCodes as List<String>
val usedCodes = currentlyUsedCurrencyCodes()
return codes
.filter { it in usedCodes }
.map { code -> CurrencyInfo(code, NSLocale.currentLocale.localizedStringForCurrencyCode(code) ?: code) }
.sortedBy { it.code }
}
/**
* `NSLocale.ISOCurrencyCodes` includes withdrawn ISO 4217 codes (Deutsche Mark, French Franc,
* ...) alongside currently-circulating ones. The currency each ISO country is assigned today is
* a reliable proxy for "still in use" without needing a maintained exclusion list; precious
* metals are added back in since they're actively traded but aren't tied to any country.
*/
private fun currentlyUsedCurrencyCodes(): Set<String> {
@Suppress("UNCHECKED_CAST")
val countryCodes = NSLocale.Companion.ISOCountryCodes as List<String>
return countryCodes.mapNotNullTo(mutableSetOf()) { country ->
NSLocale(localeIdentifier = "_$country").currencyCode
} + PRECIOUS_METAL_CURRENCY_CODES
}
actual fun localeCurrencyCode(): String? = NSLocale.currentLocale.currencyCode
actual fun currencyDecimalDigits(code: String): Int {
@@ -2,6 +2,14 @@ package xyz.tyiu.satsprice
data class CurrencyInfo(val code: String, val displayName: String)
/**
* ISO 4217 codes for the precious metals actively traded today. These aren't tied to any
* country, so a currently-used-currency filter derived from country/locale data (as the
* Android/JVM/Apple [systemCurrencies] implementations do, to drop long-withdrawn currencies
* like the Deutsche Mark) would otherwise exclude them too.
*/
val PRECIOUS_METAL_CURRENCY_CODES: Set<String> = setOf("XAU", "XAG", "XPD", "XPT")
/** All ISO 4217 currencies the current platform knows about, with localized display names. */
expect fun systemCurrencies(): List<CurrencyInfo>
@@ -5,9 +5,27 @@ import java.util.Locale
actual fun systemCurrencies(): List<CurrencyInfo> =
Currency.getAvailableCurrencies()
.filter { it.currencyCode in currentlyUsedCurrencyCodes() }
.map { CurrencyInfo(it.currencyCode, it.getDisplayName(Locale.getDefault())) }
.sortedBy { it.code }
/**
* `Currency.getAvailableCurrencies()` includes every ISO 4217 code the JDK has ever known about —
* historical currencies (Deutsche Mark, French Franc, ...) and test/placeholder codes (XTS, XXX)
* included. The currency currently assigned to each ISO country is a reliable proxy for "still in
* use" without needing a maintained exclusion list; precious metals are added back in since
* they're actively traded but aren't tied to any country.
*/
@Suppress("DEPRECATION") // Locale(language, country) still works fine; Locale.of() needs newer Android API levels.
private fun currentlyUsedCurrencyCodes(): Set<String> =
Locale.getISOCountries().mapNotNullTo(mutableSetOf()) { country ->
try {
Currency.getInstance(Locale("", country))?.currencyCode
} catch (e: IllegalArgumentException) {
null
}
} + PRECIOUS_METAL_CURRENCY_CODES
actual fun localeCurrencyCode(): String? = try {
Currency.getInstance(Locale.getDefault())?.currencyCode
} catch (e: IllegalArgumentException) {
@@ -0,0 +1,29 @@
package xyz.tyiu.satsprice
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
* `java.util.Currency.getAvailableCurrencies()` includes every ISO 4217 code the JDK has ever
* known about — withdrawn currencies and test/placeholder codes included — so [systemCurrencies]
* filters those out, while keeping precious metals, which are still actively traded.
*/
class SystemCurrenciesJvmTest {
@Test
fun systemCurrencies_excludesWithdrawnAndNonCountryCodes() {
val codes = systemCurrencies().map { it.code }.toSet()
assertFalse("DEM" in codes, "Deutsche Mark was withdrawn when Germany adopted the Euro")
assertFalse("FRF" in codes, "French Franc was withdrawn when France adopted the Euro")
assertFalse("XTS" in codes, "XTS is reserved for testing, not a real currency")
assertFalse("XXX" in codes, "XXX represents 'no currency', not a real currency")
assertTrue("USD" in codes)
assertTrue("EUR" in codes)
assertTrue("ALL" in codes)
assertTrue("XAU" in codes, "Gold is still actively traded even though it isn't tied to a country")
assertTrue("XAG" in codes, "Silver is still actively traded even though it isn't tied to a country")
}
}