Skip to content

Commit c09c1ba

Browse files
committed
add currency formatter stuff from #6653 (salvaged it because it seems abandoned)
1 parent 756b584 commit c09c1ba

File tree

6 files changed

+251
-0
lines changed

6 files changed

+251
-0
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package de.westnordost.streetcomplete.util.locale
2+
3+
import java.text.NumberFormat
4+
import java.util.Locale
5+
6+
actual class CurrencyFormatter actual constructor(locale: androidx.compose.ui.text.intl.Locale?) {
7+
8+
private val formatter =
9+
if (locale == null) NumberFormat.getCurrencyInstance()
10+
else NumberFormat.getCurrencyInstance(locale.platformLocale)
11+
12+
actual fun format(value: Double): String =
13+
formatter.format(value)
14+
15+
actual val currencyCode: String? get() = formatter.currency?.currencyCode
16+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package de.westnordost.streetcomplete.util.locale
2+
3+
import androidx.compose.ui.text.intl.Locale
4+
5+
/**
6+
* Information about how a currency should be formatted in the current locale
7+
*/
8+
data class CurrencyFormatElements(
9+
/** The currency symbol (e.g. "€", "$", "£") */
10+
val symbol: String,
11+
/** Whether the symbol comes before the amount (true for "$10", false for "10 €") */
12+
val isSymbolBeforeAmount: Boolean,
13+
/** Whether there is a whitespace between the currency symbol and the amount */
14+
val hasWhitespace: Boolean,
15+
/** Number of decimal places (e.g. 2 for EUR/USD, 0 for JPY) */
16+
val decimalDigits: Int,
17+
/** Decimal separator, e.g. the comma in "1.500,00$" */
18+
val decimalSeparator: Char?,
19+
/** Grouping separator, e.g. the dot in "1.500,00$" */
20+
val groupingSeparator: Char?
21+
) {
22+
companion object {
23+
fun of(locale: Locale?): CurrencyFormatElements =
24+
ofOrNull(locale) ?: defaultFallback(locale)
25+
26+
private fun ofOrNull(locale: Locale?): CurrencyFormatElements? {
27+
val formatter = CurrencyFormatter(locale)
28+
val d = "\\p{Nd}" // digit
29+
val a = "[^\\p{Nd}]" // not a digit
30+
// e.g. US $ 1 , 500 . 00
31+
// or NO kr 1 ␣ 500 , 00
32+
// or DE 1 . 500 , 00 €
33+
// or JP ¥ 1 , 500
34+
val regex = Regex("($a+)?$d($a)?$d{3}(?:($a)($d+))?($a+)?")
35+
val matchResult = regex.matchEntire(formatter.format(1500.00)) ?: return null
36+
val values = matchResult.groupValues
37+
val symbolBefore = values[1].takeIf { it.isNotEmpty() }
38+
val groupingSeparator = values[2].firstOrNull()
39+
val decimalSeparator = values[3].firstOrNull()
40+
val fractionDigits = values[4].length
41+
val symbolAfter = values[5].takeIf { it.isNotEmpty() }
42+
43+
// huh, there's either something both in front and end or neither? Don't know what this is, then!
44+
if (symbolAfter != null && symbolBefore != null) return null
45+
val symbol = symbolBefore ?: symbolAfter ?: return null
46+
val symbolOnly = symbol.trim()
47+
48+
return CurrencyFormatElements(
49+
symbol = symbolOnly,
50+
isSymbolBeforeAmount = symbolBefore != null,
51+
hasWhitespace = symbolOnly != symbol,
52+
decimalDigits = fractionDigits,
53+
decimalSeparator = decimalSeparator,
54+
groupingSeparator = groupingSeparator,
55+
)
56+
}
57+
58+
private fun defaultFallback(locale: Locale?): CurrencyFormatElements {
59+
val numberFormatter = NumberFormatter(locale)
60+
return CurrencyFormatElements(
61+
symbol = "¤",
62+
isSymbolBeforeAmount = true,
63+
hasWhitespace = true,
64+
decimalDigits = 2,
65+
decimalSeparator = numberFormatter.decimalSeparator,
66+
groupingSeparator = numberFormatter.groupingSeparator
67+
)
68+
}
69+
}
70+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package de.westnordost.streetcomplete.util.locale
2+
3+
import androidx.compose.ui.text.intl.Locale
4+
5+
/**
6+
* Locale-aware formatting of currencies
7+
*
8+
* @param locale Locale to use. If [locale] is `null`, the default locale (for formatting) will be
9+
* used. Note that the region **must** be specified in the locale for this formatter to format
10+
* correctly, otherwise it doesn't know which currency to use
11+
*/
12+
expect class CurrencyFormatter(locale: Locale? = null) {
13+
/**
14+
* @param value the value to format, e.g. 3.0
15+
* @return the formatted input value, e.g. € 3.00
16+
*/
17+
fun format(value: Double): String
18+
19+
/** ISO 4217 currency code */
20+
val currencyCode: String?
21+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package de.westnordost.streetcomplete.util.locale
2+
3+
import androidx.compose.ui.text.intl.Locale
4+
import kotlin.test.Test
5+
import kotlin.test.assertEquals
6+
7+
class CurrencyFormatElementsTest {
8+
9+
@Test fun `of Germany Euro`() {
10+
assertEquals(
11+
CurrencyFormatElements(
12+
symbol = "",
13+
isSymbolBeforeAmount = false,
14+
hasWhitespace = true,
15+
decimalDigits = 2,
16+
decimalSeparator = ',',
17+
groupingSeparator = '.',
18+
),
19+
CurrencyFormatElements.of(Locale("de-DE"))
20+
)
21+
}
22+
23+
@Test fun `of Ireland Euro`() {
24+
assertEquals(
25+
CurrencyFormatElements(
26+
symbol = "",
27+
isSymbolBeforeAmount = true,
28+
hasWhitespace = false,
29+
decimalDigits = 2,
30+
decimalSeparator = '.',
31+
groupingSeparator = ',',
32+
),
33+
CurrencyFormatElements.of(Locale("en-IE"))
34+
)
35+
}
36+
37+
@Test fun `of Japan Yen`() {
38+
assertEquals(
39+
CurrencyFormatElements(
40+
symbol = "",
41+
isSymbolBeforeAmount = true,
42+
hasWhitespace = false,
43+
decimalDigits = 0,
44+
decimalSeparator = null,
45+
groupingSeparator = ',',
46+
),
47+
CurrencyFormatElements.of(Locale("ja-JP"))
48+
)
49+
}
50+
51+
@Test fun `of US Dollar`() {
52+
assertEquals(
53+
CurrencyFormatElements(
54+
symbol = "$",
55+
isSymbolBeforeAmount = true,
56+
hasWhitespace = false,
57+
decimalDigits = 2,
58+
decimalSeparator = '.',
59+
groupingSeparator = ',',
60+
),
61+
CurrencyFormatElements.of(Locale("en-US"))
62+
)
63+
}
64+
65+
@Test fun `of Norway Krona`() {
66+
assertEquals(
67+
CurrencyFormatElements(
68+
symbol = "kr",
69+
isSymbolBeforeAmount = true,
70+
hasWhitespace = true,
71+
decimalDigits = 2,
72+
decimalSeparator = ',',
73+
groupingSeparator = '\u00A0',
74+
),
75+
CurrencyFormatElements.of(Locale("nb-NO"))
76+
)
77+
}
78+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package de.westnordost.streetcomplete.util.locale
2+
3+
import androidx.compose.ui.text.intl.Locale
4+
import kotlin.test.Test
5+
import kotlin.test.assertEquals
6+
7+
internal class CurrencyFormatterTest {
8+
@Test fun `euro in Germany`() {
9+
val f = formatter("de-DE")
10+
assertEquals("1.538,00\u00A0", f.format(1538.0))
11+
assertEquals("EUR", f.currencyCode)
12+
}
13+
14+
@Test fun `Ireland euro in Ireland`() {
15+
val f = formatter("en-IE")
16+
assertEquals("€1,538.00", f.format(1538.0))
17+
assertEquals("EUR", f.currencyCode)
18+
}
19+
20+
@Test fun `yen in Japan`() {
21+
val f = formatter("ja-JP")
22+
assertEquals("¥1,538", f.format(1538.00))
23+
assertEquals("JPY", f.currencyCode)
24+
}
25+
26+
@Test fun `dollar in US`() {
27+
val f = formatter("en-US")
28+
assertEquals("$1,538.00", f.format(1538.00))
29+
assertEquals("USD", f.currencyCode)
30+
}
31+
32+
@Test fun `krona in Norway`() {
33+
val f = formatter("nb-NO")
34+
assertEquals("kr\u00A01\u00A0538,00", f.format(1538.00))
35+
assertEquals("NOK", f.currencyCode)
36+
}
37+
38+
@Test fun `riyal in Saudi Arabia`() {
39+
val f = formatter("ar-SA")
40+
assertEquals("\u200F١٬٥٣٨٫٠٠\u00A0ر.س.\u200F", f.format(1538.00))
41+
assertEquals("SAR", f.currencyCode)
42+
}
43+
44+
private fun formatter(localeTag: String) =
45+
CurrencyFormatter(Locale(localeTag))
46+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package de.westnordost.streetcomplete.util.locale
2+
3+
import androidx.compose.ui.text.intl.Locale
4+
import platform.Foundation.NSLocaleCurrencyCode
5+
import platform.Foundation.NSNumber
6+
import platform.Foundation.NSNumberFormatter
7+
import platform.Foundation.NSNumberFormatterCurrencyStyle
8+
9+
actual class CurrencyFormatter actual constructor(locale: Locale?) {
10+
11+
private val formatter = NSNumberFormatter().also {
12+
if (locale != null) it.locale = locale.platformLocale
13+
it.numberStyle = NSNumberFormatterCurrencyStyle
14+
}
15+
16+
actual fun format(value: Double): String =
17+
formatter.stringFromNumber(NSNumber(value)) ?: ""
18+
19+
actual val currencyCode: String? get() = formatter.currencyCode
20+
}

0 commit comments

Comments
 (0)