Match currency search against issuing country names too
Searching "Japan" now finds JPY, "Canada" finds CAD, etc. — matching just the currency's own display name wasn't enough, since e.g. "Canadian Dollar" doesn't contain "Canada" as a substring. Adds a per-platform regionDisplayName(regionCode) (java.util.Locale on JVM/Android, NSLocale on Apple, Intl.DisplayNames on web) and reuses CurrencyFlag.kt's currency-to-country-code derivation (now extracted as issuingCountryCodes) to look up the country/countries behind a currency code. "EU" is hardcoded to "European Union" since it's not a real ISO 3166-1 code platform locale data can resolve. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
This commit is contained in:
@@ -37,3 +37,9 @@ actual fun currencyDecimalDigits(code: String): Int = try {
|
|||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
2
|
2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION") // Locale(language, country) still works fine; Locale.of() needs newer Android API levels.
|
||||||
|
actual fun regionDisplayName(regionCode: String): String? {
|
||||||
|
if (regionCode !in Locale.getISOCountries()) return null
|
||||||
|
return Locale("", regionCode).getDisplayCountry(Locale.getDefault())
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,3 +34,6 @@ actual fun currencyDecimalDigits(code: String): Int {
|
|||||||
formatter.currencyCode = code
|
formatter.currencyCode = code
|
||||||
return formatter.maximumFractionDigits.toInt()
|
return formatter.maximumFractionDigits.toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun regionDisplayName(regionCode: String): String? =
|
||||||
|
NSLocale.currentLocale.localizedStringForCountryCode(regionCode)
|
||||||
|
|||||||
@@ -16,24 +16,31 @@ private val MULTI_COUNTRY_CURRENCY_REGION_CODES: Map<String, List<String>> = map
|
|||||||
private const val MAX_FLAGS_PER_CURRENCY = 3
|
private const val MAX_FLAGS_PER_CURRENCY = 3
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A country flag emoji for [code], derived from the first two letters of the ISO 4217 code —
|
* ISO 3166-1 alpha-2 codes of every country that issues [code], derived from the first two
|
||||||
* which double as the issuing country's ISO 3166-1 alpha-2 code for ordinary national
|
* letters of the ISO 4217 code — which double as the issuing country's ISO 3166-1 alpha-2 code
|
||||||
* currencies — or null when there's no flag to show. [MULTI_COUNTRY_CURRENCY_REGION_CODES]
|
* for ordinary national currencies — or empty when [code] isn't tied to any country.
|
||||||
* lists every country sharing a currency with no single issuer, so those show all their flags
|
* [MULTI_COUNTRY_CURRENCY_REGION_CODES] lists every country sharing a currency with no single
|
||||||
* side by side, unless there are more than [MAX_FLAGS_PER_CURRENCY] of them; the remaining
|
* issuer; the remaining "X"-prefixed codes are precious metals, testing codes, and other
|
||||||
* "X"-prefixed codes are precious metals, testing codes, and other non-national codes (XAU, XTS,
|
* non-national codes (XAU, XTS, XXX, ...), none of which has a country. EUR is a special case,
|
||||||
* XXX, ...), none of which has a country to show. EUR is a special case handled separately, using
|
* using the EU's own region code. Shared by [currencyFlagEmoji] and currency search-by-country.
|
||||||
* the EU's own flag.
|
*/
|
||||||
|
internal fun issuingCountryCodes(code: String): List<String> {
|
||||||
|
MULTI_COUNTRY_CURRENCY_REGION_CODES[code]?.let { return it }
|
||||||
|
if (code.startsWith("X")) return emptyList()
|
||||||
|
val regionCode = if (code == "EUR") "EU" else code.take(2)
|
||||||
|
if (regionCode.length != 2 || regionCode.any { it !in 'A'..'Z' }) return emptyList()
|
||||||
|
return listOf(regionCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A country flag emoji for [code], or null when there's no flag to show — either because [code]
|
||||||
|
* isn't tied to any country, or because it's shared by more than [MAX_FLAGS_PER_CURRENCY]
|
||||||
|
* countries, which would be too visually noisy to show side by side.
|
||||||
*/
|
*/
|
||||||
fun currencyFlagEmoji(code: String): String? {
|
fun currencyFlagEmoji(code: String): String? {
|
||||||
MULTI_COUNTRY_CURRENCY_REGION_CODES[code]?.let { regionCodes ->
|
val regionCodes = issuingCountryCodes(code)
|
||||||
if (regionCodes.size > MAX_FLAGS_PER_CURRENCY) return null
|
if (regionCodes.isEmpty() || regionCodes.size > MAX_FLAGS_PER_CURRENCY) return null
|
||||||
return regionCodes.joinToString(" ") { regionFlagEmoji(it) }
|
return regionCodes.joinToString(" ") { regionFlagEmoji(it) }
|
||||||
}
|
|
||||||
if (code.startsWith("X")) return null
|
|
||||||
val regionCode = if (code == "EUR") "EU" else code.take(2)
|
|
||||||
if (regionCode.length != 2 || regionCode.any { it !in 'A'..'Z' }) return null
|
|
||||||
return regionFlagEmoji(regionCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The flag emoji for the 2-letter ISO 3166-1 alpha-2 [regionCode]. */
|
/** The flag emoji for the 2-letter ISO 3166-1 alpha-2 [regionCode]. */
|
||||||
|
|||||||
@@ -3,14 +3,31 @@ package xyz.tyiu.satsprice
|
|||||||
data class CurrencyInfo(val code: String, val displayName: String)
|
data class CurrencyInfo(val code: String, val displayName: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether [info]'s code or display name contains [query], case-insensitively. A plain function
|
* Whether [info]'s code, display name, or the localized name of any country that issues it (e.g.
|
||||||
* (rather than a `CurrencyInfo` extension) so Kotlin/Native exports a predictable, positionally
|
* "Japan" for JPY) contains [query], case-insensitively. A plain function (rather than a
|
||||||
* clear Swift signature, matching [currencyFlagEmoji]'s style.
|
* `CurrencyInfo` extension) so Kotlin/Native exports a predictable, positionally clear Swift
|
||||||
|
* signature, matching [currencyFlagEmoji]'s style.
|
||||||
*/
|
*/
|
||||||
fun matchesCurrencySearch(info: CurrencyInfo, query: String): Boolean =
|
fun matchesCurrencySearch(info: CurrencyInfo, query: String): Boolean {
|
||||||
query.isBlank() ||
|
if (query.isBlank()) return true
|
||||||
info.code.contains(query, ignoreCase = true) ||
|
if (info.code.contains(query, ignoreCase = true)) return true
|
||||||
info.displayName.contains(query, ignoreCase = true)
|
if (info.displayName.contains(query, ignoreCase = true)) return true
|
||||||
|
return issuingCountryCodes(info.code).any { regionCode ->
|
||||||
|
localizedRegionName(regionCode)?.contains(query, ignoreCase = true) == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A localized display name for ISO 3166-1 alpha-2 [regionCode] (e.g. "Canada"), or null if the
|
||||||
|
* platform doesn't recognize it. "EU" is handled directly since it's not a real ISO 3166-1
|
||||||
|
* country code — it's [issuingCountryCodes]' own stand-in for EUR's region/flag — and platform
|
||||||
|
* locale data doesn't reliably resolve it to a name the way it does real country codes.
|
||||||
|
*/
|
||||||
|
private fun localizedRegionName(regionCode: String): String? =
|
||||||
|
if (regionCode == "EU") "European Union" else regionDisplayName(regionCode)
|
||||||
|
|
||||||
|
/** The current platform's localized display name for ISO 3166-1 alpha-2 [regionCode], if known. */
|
||||||
|
expect fun regionDisplayName(regionCode: String): String?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ISO 4217 codes for the precious metals actively traded today. These aren't tied to any
|
* ISO 4217 codes for the precious metals actively traded today. These aren't tied to any
|
||||||
|
|||||||
@@ -36,4 +36,28 @@ class SystemCurrenciesTest {
|
|||||||
assertFalse(matchesCurrencySearch(usd, "EUR"))
|
assertFalse(matchesCurrencySearch(usd, "EUR"))
|
||||||
assertFalse(matchesCurrencySearch(usd, "Euro"))
|
assertFalse(matchesCurrencySearch(usd, "Euro"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun matchesCurrencySearch_matchesByIssuingCountryName() {
|
||||||
|
// "Canadian Dollar" doesn't contain "Canada" as a substring, so this only passes if the
|
||||||
|
// country-name path (rather than just code/display-name matching) is actually consulted.
|
||||||
|
val cad = CurrencyInfo("CAD", "Canadian Dollar")
|
||||||
|
val countryName = regionDisplayName("CA")
|
||||||
|
assertTrue(countryName != null && countryName.isNotBlank(), "expected a display name for CA")
|
||||||
|
|
||||||
|
assertTrue(matchesCurrencySearch(cad, countryName))
|
||||||
|
assertTrue(matchesCurrencySearch(cad, countryName.lowercase()))
|
||||||
|
assertFalse(matchesCurrencySearch(cad, "Definitely not a matching country name"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun matchesCurrencySearch_matchesEurozoneByHardcodedEuropeanUnionName() {
|
||||||
|
// "EU" isn't a real ISO 3166-1 country code, so platform locale data can't be relied on
|
||||||
|
// to name it — this is hardcoded rather than delegated to [regionDisplayName].
|
||||||
|
val eur = CurrencyInfo("EUR", "Euro")
|
||||||
|
|
||||||
|
assertTrue(matchesCurrencySearch(eur, "European"))
|
||||||
|
assertTrue(matchesCurrencySearch(eur, "union"))
|
||||||
|
assertFalse(matchesCurrencySearch(eur, "Germany"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,3 +37,9 @@ actual fun currencyDecimalDigits(code: String): Int = try {
|
|||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
2
|
2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION") // Locale(language, country) still works fine; Locale.of() needs newer Android API levels.
|
||||||
|
actual fun regionDisplayName(regionCode: String): String? {
|
||||||
|
if (regionCode !in Locale.getISOCountries()) return null
|
||||||
|
return Locale("", regionCode).getDisplayCountry(Locale.getDefault())
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,3 +36,20 @@ actual fun currencyDecimalDigits(code: String): Int = try {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
2
|
2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Falls back to the region code itself (rather than a JS null/undefined) when unrecognized, since
|
||||||
|
// a plain `-> String?` return type doesn't reliably round-trip through js() interop here; that
|
||||||
|
// fallback is filtered back out to null actual-side below, same as the other platforms.
|
||||||
|
private fun jsRegionDisplayName(regionCode: String): String = js(
|
||||||
|
"""(function() {
|
||||||
|
try {
|
||||||
|
var names = new Intl.DisplayNames(['en'], { type: 'region' });
|
||||||
|
return names.of(regionCode) || regionCode;
|
||||||
|
} catch (e) {
|
||||||
|
return regionCode;
|
||||||
|
}
|
||||||
|
})()""",
|
||||||
|
)
|
||||||
|
|
||||||
|
actual fun regionDisplayName(regionCode: String): String? =
|
||||||
|
jsRegionDisplayName(regionCode).takeIf { it != regionCode }
|
||||||
|
|||||||
Reference in New Issue
Block a user