diff --git a/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.android.kt b/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.android.kt index 2b1dd63..10264b1 100644 --- a/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.android.kt +++ b/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.android.kt @@ -5,9 +5,27 @@ import java.util.Locale actual fun systemCurrencies(): List = 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 = + 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) { diff --git a/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.apple.kt b/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.apple.kt index 5aeb877..41dd0a3 100644 --- a/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.apple.kt +++ b/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.apple.kt @@ -5,11 +5,27 @@ import platform.Foundation.* actual fun systemCurrencies(): List { @Suppress("UNCHECKED_CAST") val codes = NSLocale.Companion.ISOCurrencyCodes as List + 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 { + @Suppress("UNCHECKED_CAST") + val countryCodes = NSLocale.Companion.ISOCountryCodes as List + 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 { diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.kt index 4325d3e..9fe195a 100644 --- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.kt +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.kt @@ -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 = setOf("XAU", "XAG", "XPD", "XPT") + /** All ISO 4217 currencies the current platform knows about, with localized display names. */ expect fun systemCurrencies(): List diff --git a/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.jvm.kt b/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.jvm.kt index 2b1dd63..10264b1 100644 --- a/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.jvm.kt +++ b/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.jvm.kt @@ -5,9 +5,27 @@ import java.util.Locale actual fun systemCurrencies(): List = 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 = + 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) { diff --git a/shared/src/jvmTest/kotlin/xyz/tyiu/satsprice/SystemCurrenciesJvmTest.kt b/shared/src/jvmTest/kotlin/xyz/tyiu/satsprice/SystemCurrenciesJvmTest.kt new file mode 100644 index 0000000..741a318 --- /dev/null +++ b/shared/src/jvmTest/kotlin/xyz/tyiu/satsprice/SystemCurrenciesJvmTest.kt @@ -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") + } +}