diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/QuestsModule.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/QuestsModule.kt index 07c293db219..2edfa6f03f4 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/QuestsModule.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/QuestsModule.kt @@ -59,13 +59,14 @@ import de.westnordost.streetcomplete.quests.bus_stop_name.AddBusStopName import de.westnordost.streetcomplete.quests.bus_stop_ref.AddBusStopRef import de.westnordost.streetcomplete.quests.bus_stop_shelter.AddBusStopShelter import de.westnordost.streetcomplete.quests.camera_type.AddCameraType +import de.westnordost.streetcomplete.quests.camping.AddCabins import de.westnordost.streetcomplete.quests.camping.AddCampDrinkingWater import de.westnordost.streetcomplete.quests.camping.AddCampPower import de.westnordost.streetcomplete.quests.camping.AddCampShower -import de.westnordost.streetcomplete.quests.camping.AddTents -import de.westnordost.streetcomplete.quests.camping.AddCabins import de.westnordost.streetcomplete.quests.camping.AddCaravans +import de.westnordost.streetcomplete.quests.camping.AddTents import de.westnordost.streetcomplete.quests.car_wash_type.AddCarWashType +import de.westnordost.streetcomplete.quests.charge.AddParkingCharge import de.westnordost.streetcomplete.quests.charging_station_bicycles.AddChargingStationBicycles import de.westnordost.streetcomplete.quests.charging_station_capacity.AddChargingStationBicycleCapacity import de.westnordost.streetcomplete.quests.charging_station_capacity.AddChargingStationCapacity @@ -293,6 +294,7 @@ fun questTypeRegistry( 17 to AddParkingType(), 18 to AddParkingAccess(), // used by OSM Carto, mapy.cz, OSMand, Sputnik etc 19 to AddParkingFee(), // used by OsmAnd + 199 to AddParkingCharge(), 20 to AddTrafficCalmingType(), diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/AddParkingCharge.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/AddParkingCharge.kt new file mode 100644 index 00000000000..10d25df05ee --- /dev/null +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/AddParkingCharge.kt @@ -0,0 +1,47 @@ +package de.westnordost.streetcomplete.quests.charge + +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry +import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType +import de.westnordost.streetcomplete.data.quest.AndroidQuest +import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR +import de.westnordost.streetcomplete.osm.Tags +import de.westnordost.streetcomplete.osm.updateCheckDateForKey + +class AddParkingCharge : OsmFilterQuestType(), AndroidQuest { + + override val elementFilter = """ + nodes, ways, relations with amenity = parking + and access ~ yes|customers|public + and fee = yes + and ( + !charge and !charge:conditional + or charge older today -18 months + ) + """ + + override val changesetComment = "Add parking charges" + override val wikiLink = "Key:charge" + override val icon = R.drawable.quest_parking_charge + override val achievements = listOf(CAR) + // for now, this hint is just a proposal + override val hint = R.string.quest_parking_charge_hint + + override fun getTitle(tags: Map) = R.string.quest_parking_charge_title + + override fun createForm() = AddParkingChargeForm() + + override fun applyAnswerTo(answer: ParkingChargeAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) { + when (answer) { + is SimpleCharge -> { + // Format: "1.50 EUR/hour" + tags["charge"] = "${answer.amount} ${answer.currency}/${answer.timeUnit}" + tags.updateCheckDateForKey("charge") + } + is ItVaries -> { + tags["charge:conditional"] = answer.conditional + tags.updateCheckDateForKey("charge:conditional") + } + } + } +} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/AddParkingChargeForm.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/AddParkingChargeForm.kt new file mode 100644 index 00000000000..570656a4990 --- /dev/null +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/AddParkingChargeForm.kt @@ -0,0 +1,113 @@ +package de.westnordost.streetcomplete.quests.charge + +import android.os.Bundle +import android.view.View +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.databinding.ComposeViewBinding +import de.westnordost.streetcomplete.osm.duration.DurationUnit +import de.westnordost.streetcomplete.quests.AbstractOsmQuestForm +import de.westnordost.streetcomplete.quests.AnswerItem +import de.westnordost.streetcomplete.ui.common.ChargeInput +import de.westnordost.streetcomplete.ui.common.dialogs.TextInputDialog +import de.westnordost.streetcomplete.ui.theme.extraLargeInput +import de.westnordost.streetcomplete.ui.util.content +import de.westnordost.streetcomplete.util.locale.CurrencyFormatElements +import de.westnordost.streetcomplete.util.locale.CurrencyFormatter +import java.util.Currency +import java.util.Locale + +class AddParkingChargeForm : AbstractOsmQuestForm() { + + override val contentLayoutResId = R.layout.compose_view + private val binding by contentViewBinding(ComposeViewBinding::bind) + + private lateinit var amountState: MutableState + private lateinit var durationUnitState: MutableState + + private lateinit var showDialogState: MutableState + + override val otherAnswers: List get() = listOf( + AnswerItem(R.string.quest_parking_charge_varies) { + showDialogState.value = true + } + ) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.composeViewBase.content { + Surface { + amountState = rememberSaveable { mutableStateOf("") } + durationUnitState = rememberSaveable { mutableStateOf(DurationUnit.HOURS) } + showDialogState = rememberSaveable { mutableStateOf(false) } + + val currencyFormatInfo = remember(countryInfo) { + CurrencyFormatElements.of(countryInfo.userPreferredLocale) + } + + ProvideTextStyle(MaterialTheme.typography.extraLargeInput) { + ChargeInput( + amount = amountState.value, + onAmountChange = { + amountState.value = it + checkIsFormComplete() + }, + currencyFormatInfo = currencyFormatInfo, + durationUnit = durationUnitState.value, + onDurationUnitChange = { unit -> + durationUnitState.value = unit + checkIsFormComplete() + }, + perLabel = getString(R.string.quest_parking_charge_time_unit_label), + durationUnitDisplayNames = { unit -> unit.getDisplayName(this@AddParkingChargeForm) }, + modifier = Modifier.padding(16.dp) + ) + } + + if (showDialogState.value) { + TextInputDialog( + onDismissRequest = { showDialogState.value = false }, + onConfirmed = { description -> + applyAnswer(ItVaries(description)) + }, + title = { Text(getString(R.string.quest_parking_charge_varies_title)) }, + textInputLabel = { Text(getString(R.string.quest_parking_charge_varies_description)) } + ) + } + } + } + } + + override fun isFormComplete(): Boolean { + val amount = amountState.value.replace(',', '.') + return amount.isNotEmpty() && amount.toDoubleOrNull() != null && amount.toDouble() > 0 + } + + override fun onClickOk() { + val amount = amountState.value.replace(',', '.') + val currency = CurrencyFormatter(countryInfo.userPreferredLocale).currencyCode ?: "???" + val timeUnit = when (durationUnitState.value) { // TODO: This could be removed if DurationUnit implements toOSMValue(). + DurationUnit.HOURS -> "hour" + DurationUnit.DAYS -> "day" + DurationUnit.MINUTES -> "minute" + } + applyAnswer(SimpleCharge(amount, currency, timeUnit)) + } +} + +fun DurationUnit.getDisplayName(form: AddParkingChargeForm): String = when (this) { + DurationUnit.HOURS -> form.getString(R.string.quest_parking_charge_hour) + DurationUnit.DAYS -> form.getString(R.string.quest_parking_charge_day) + DurationUnit.MINUTES -> form.getString(R.string.quest_parking_charge_minute_short) +} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/ChargeInput.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/ChargeInput.kt new file mode 100644 index 00000000000..812a540feb1 --- /dev/null +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/ChargeInput.kt @@ -0,0 +1,132 @@ +package de.westnordost.streetcomplete.ui.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.osm.duration.DurationUnit +import de.westnordost.streetcomplete.util.locale.CurrencyFormatElements +// import org.jetbrains.compose.ui.tooling.preview.Preview + +/** A composable for inputting a charge amount with currency symbol and time unit selector */ +@Composable +fun ChargeInput( + amount: String, + onAmountChange: (String) -> Unit, + currencyFormatInfo: CurrencyFormatElements, + durationUnit: DurationUnit, + onDurationUnitChange: (DurationUnit) -> Unit, + perLabel: String, + modifier: Modifier = Modifier, + durationUnitDisplayNames: (DurationUnit) -> String +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (currencyFormatInfo.isSymbolBeforeAmount) { + Text( + text = currencyFormatInfo.symbol, + style = MaterialTheme.typography.h5, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), + modifier = Modifier.padding(end = 8.dp) + ) + } + + TextField( + value = amount, + onValueChange = onAmountChange, + placeholder = { + // Generate placeholder based on decimal places + val placeholderValue = when (currencyFormatInfo.decimalDigits) { + 0 -> "150" + 1 -> "15.0" + else -> "1.50" + } + Text(placeholderValue) + }, + keyboardOptions = KeyboardOptions( + keyboardType = if (currencyFormatInfo.decimalDigits > 0) { + KeyboardType.Decimal + } else { + KeyboardType.Number + } + ), + modifier = Modifier.width(150.dp), + singleLine = true, + ) + + if (!currencyFormatInfo.isSymbolBeforeAmount) { + Text( + text = currencyFormatInfo.symbol, + style = MaterialTheme.typography.h5, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), + modifier = Modifier.padding(start = 8.dp) + ) + } + + Text( + text = perLabel, + style = MaterialTheme.typography.body1 + ) + + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + /* + DurationUnitDropdown( + selectedDuration = durationUnit, + onSelectedDuration = onDurationUnitChange, + )*/ + DropdownButton( + items = DurationUnit.entries, + onSelectedItem = onDurationUnitChange, + itemContent = { unit -> + Text(durationUnitDisplayNames(unit)) + }, + selectedItem = durationUnit, + style = ButtonStyle.Outlined, + ) + } + } +} +/* +@Composable +@Preview +private fun ChargeInputPreview() { + val amount = remember { mutableStateOf("1.50") } + val durationUnit = remember { mutableStateOf(DurationUnit.HOURS) } + + ChargeInput( + amount = amount.value, + onAmountChange = { amount.value = it }, + currencyFormatInfo = CurrencyFormatElements( + symbol = "€", + symbolBeforeAmount = false, + decimalPlaces = 2 + ), + durationUnit = durationUnit.value, + onDurationUnitChange = { durationUnit.value = it }, + perLabel = "per", + durationUnitDisplayNames = { unit -> + when (unit) { + DurationUnit.MINUTES -> Res.string.unit_minutes + DurationUnit.HOURS -> Res.string.unit_hours + DurationUnit.DAYS -> Res.string.unit_days + } as String + } + ) +} +*/ diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/ParkingChargeAnswer.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/ParkingChargeAnswer.kt new file mode 100644 index 00000000000..9c8927c56f1 --- /dev/null +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/ParkingChargeAnswer.kt @@ -0,0 +1,16 @@ +package de.westnordost.streetcomplete.quests.charge + +sealed interface ParkingChargeAnswer + +data class SimpleCharge( + // e.g. "1.50" + val amount: String, + // e.g. "EUR" + val currency: String, + // either "day", "hour" or "minute" + val timeUnit: String +) : ParkingChargeAnswer + +data class ItVaries( + val conditional: String +) : ParkingChargeAnswer diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatter.android.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatter.android.kt new file mode 100644 index 00000000000..c561a51648c --- /dev/null +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatter.android.kt @@ -0,0 +1,16 @@ +package de.westnordost.streetcomplete.util.locale + +import java.text.NumberFormat +import java.util.Locale + +actual class CurrencyFormatter actual constructor(locale: androidx.compose.ui.text.intl.Locale?) { + + private val formatter = + if (locale == null) NumberFormat.getCurrencyInstance() + else NumberFormat.getCurrencyInstance(locale.platformLocale) + + actual fun format(value: Double): String = + formatter.format(value) + + actual val currencyCode: String? get() = formatter.currency?.currencyCode +} diff --git a/app/src/androidMain/res/drawable/quest_parking_charge.xml b/app/src/androidMain/res/drawable/quest_parking_charge.xml new file mode 100644 index 00000000000..763352ca8bc --- /dev/null +++ b/app/src/androidMain/res/drawable/quest_parking_charge.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + diff --git a/app/src/androidMain/res/values-de/strings.xml b/app/src/androidMain/res/values-de/strings.xml index 1e22562ba9b..a7c54f41cea 100644 --- a/app/src/androidMain/res/values-de/strings.xml +++ b/app/src/androidMain/res/values-de/strings.xml @@ -912,6 +912,18 @@ Falls du von der Flut der Aufgaben überwältigt bist, kannst du in den Einstell "Alle Personen der Öffentlichkeit" "Jede Person, die einen bestimmten Ort besucht (z. B. nur Kunden)" "Nur Einzelne mit Erlaubnis" + + + Wie viel muss man bezahlen, um hier zu parken? + Variable Preise + Es variiert je nach Stunde, Tag, … + pro + Stunde + Tag + Beschreibe, wie sich die Parkgebühren unterscheiden (z.B. "1 EUR/h weekdays, 2 EUR/h weekends") + min + "Bei dieser Aufgabe wird der Einfachheit halber zunächst empfohlen, bei unterschiedlichen Beträgen den Standardbetrag anzugeben. Wenn ein Parkplatz beispielsweise tagsüber 3 € pro Stunde und nachts 1 € pro Stunde kostet, werden 3 € pro Stunde eingegeben. Das erleichtert später den Vergleich der Preise anhand der Daten." + "Stimmen die Leerungszeiten für diesen Postkasten noch?" "Was für ein Fahrradhindernis ist das?" "Diagonal zum Weg" @@ -1475,4 +1487,4 @@ Zum Beispiel 1,3 oder 2b,4,6." "PET-Flaschen" "Schild hinzufügen" "Schild entfernen" - \ No newline at end of file + diff --git a/app/src/androidMain/res/values/strings.xml b/app/src/androidMain/res/values/strings.xml index 321cc02392a..e1b873541fa 100644 --- a/app/src/androidMain/res/values/strings.xml +++ b/app/src/androidMain/res/values/strings.xml @@ -1413,6 +1413,17 @@ If there are no signs along the whole street which apply for the highlighted sec Any person visiting a specific place (e.g. customers only) Only individual(s) with permission + + Variable pricing + How much do you need to pay to park here? + It varies… + per + hour + day + Describe how the parking charges vary (e.g. "1 EUR/h weekdays, 2 EUR/h weekends") + min + "For simplicity’s sake, it is recommended that you enter the standard amount for different amounts in this quest. For example, if it costs €3 during the day and €1 at night, enter €3. This makes it easier to compare prices from the data later on." + Do you have to pay to park here? Depends on time and day… Yes,… diff --git a/app/src/androidUnitTest/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatterTest.kt b/app/src/androidUnitTest/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatterTest.kt new file mode 100644 index 00000000000..a9b1f0b0d3f --- /dev/null +++ b/app/src/androidUnitTest/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatterTest.kt @@ -0,0 +1,46 @@ +package de.westnordost.streetcomplete.util.locale + +import androidx.compose.ui.text.intl.Locale +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class CurrencyFormatterTest { + @Test fun `euro in Germany`() { + val f = formatter("de-DE") + assertEquals("1.538,00\u00A0€", f.format(1538.0)) + assertEquals("EUR", f.currencyCode) + } + + @Test fun `Ireland euro in Ireland`() { + val f = formatter("en-IE") + assertEquals("€1,538.00", f.format(1538.0)) + assertEquals("EUR", f.currencyCode) + } + + @Test fun `yen in Japan`() { + val f = formatter("ja-JP") + assertEquals("¥1,538", f.format(1538.00)) + assertEquals("JPY", f.currencyCode) + } + + @Test fun `dollar in US`() { + val f = formatter("en-US") + assertEquals("$1,538.00", f.format(1538.00)) + assertEquals("USD", f.currencyCode) + } + + @Test fun `krona in Norway`() { + val f = formatter("nb-NO") + assertEquals("kr\u00A01\u00A0538,00", f.format(1538.00)) + assertEquals("NOK", f.currencyCode) + } + + @Test fun `riyal in Saudi Arabia`() { + val f = formatter("ar-SA") + assertEquals("\u200F١٬٥٣٨٫٠٠\u00A0ر.س.\u200F", f.format(1538.00)) + assertEquals("SAR", f.currencyCode) + } + + private fun formatter(localeTag: String) = + CurrencyFormatter(Locale(localeTag)) +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatElements.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatElements.kt new file mode 100644 index 00000000000..4b27cd0d81e --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatElements.kt @@ -0,0 +1,70 @@ +package de.westnordost.streetcomplete.util.locale + +import androidx.compose.ui.text.intl.Locale + +/** + * Information about how a currency should be formatted in the current locale + */ +data class CurrencyFormatElements( + /** The currency symbol (e.g. "€", "$", "£") */ + val symbol: String, + /** Whether the symbol comes before the amount (true for "$10", false for "10 €") */ + val isSymbolBeforeAmount: Boolean, + /** Whether there is a whitespace between the currency symbol and the amount */ + val hasWhitespace: Boolean, + /** Number of decimal places (e.g. 2 for EUR/USD, 0 for JPY) */ + val decimalDigits: Int, + /** Decimal separator, e.g. the comma in "1.500,00$" */ + val decimalSeparator: Char?, + /** Grouping separator, e.g. the dot in "1.500,00$" */ + val groupingSeparator: Char? +) { + companion object { + fun of(locale: Locale?): CurrencyFormatElements = + ofOrNull(locale) ?: defaultFallback(locale) + + private fun ofOrNull(locale: Locale?): CurrencyFormatElements? { + val formatter = CurrencyFormatter(locale) + val d = "\\p{Nd}" // digit + val a = "[^\\p{Nd}]" // not a digit + // e.g. US $ 1 , 500 . 00 + // or NO kr 1 ␣ 500 , 00 + // or DE 1 . 500 , 00 € + // or JP ¥ 1 , 500 + val regex = Regex("($a+)?$d($a)?$d{3}(?:($a)($d+))?($a+)?") + val matchResult = regex.matchEntire(formatter.format(1500.00)) ?: return null + val values = matchResult.groupValues + val symbolBefore = values[1].takeIf { it.isNotEmpty() } + val groupingSeparator = values[2].firstOrNull() + val decimalSeparator = values[3].firstOrNull() + val fractionDigits = values[4].length + val symbolAfter = values[5].takeIf { it.isNotEmpty() } + + // huh, there's either something both in front and end or neither? Don't know what this is, then! + if (symbolAfter != null && symbolBefore != null) return null + val symbol = symbolBefore ?: symbolAfter ?: return null + val symbolOnly = symbol.trim() + + return CurrencyFormatElements( + symbol = symbolOnly, + isSymbolBeforeAmount = symbolBefore != null, + hasWhitespace = symbolOnly != symbol, + decimalDigits = fractionDigits, + decimalSeparator = decimalSeparator, + groupingSeparator = groupingSeparator, + ) + } + + private fun defaultFallback(locale: Locale?): CurrencyFormatElements { + val numberFormatter = NumberFormatter(locale) + return CurrencyFormatElements( + symbol = "¤", + isSymbolBeforeAmount = true, + hasWhitespace = true, + decimalDigits = 2, + decimalSeparator = numberFormatter.decimalSeparator, + groupingSeparator = numberFormatter.groupingSeparator + ) + } + } +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatter.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatter.kt new file mode 100644 index 00000000000..8a7c22b0899 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatter.kt @@ -0,0 +1,21 @@ +package de.westnordost.streetcomplete.util.locale + +import androidx.compose.ui.text.intl.Locale + +/** + * Locale-aware formatting of currencies + * + * @param locale Locale to use. If [locale] is `null`, the default locale (for formatting) will be + * used. Note that the region **must** be specified in the locale for this formatter to format + * correctly, otherwise it doesn't know which currency to use + */ +expect class CurrencyFormatter(locale: Locale? = null) { + /** + * @param value the value to format, e.g. 3.0 + * @return the formatted input value, e.g. € 3.00 + */ + fun format(value: Double): String + + /** ISO 4217 currency code */ + val currencyCode: String? +} diff --git a/app/src/commonTest/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatElementsTest.kt b/app/src/commonTest/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatElementsTest.kt new file mode 100644 index 00000000000..0944515f922 --- /dev/null +++ b/app/src/commonTest/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatElementsTest.kt @@ -0,0 +1,78 @@ +package de.westnordost.streetcomplete.util.locale + +import androidx.compose.ui.text.intl.Locale +import kotlin.test.Test +import kotlin.test.assertEquals + +class CurrencyFormatElementsTest { + + @Test fun `of Germany Euro`() { + assertEquals( + CurrencyFormatElements( + symbol = "€", + isSymbolBeforeAmount = false, + hasWhitespace = true, + decimalDigits = 2, + decimalSeparator = ',', + groupingSeparator = '.', + ), + CurrencyFormatElements.of(Locale("de-DE")) + ) + } + + @Test fun `of Ireland Euro`() { + assertEquals( + CurrencyFormatElements( + symbol = "€", + isSymbolBeforeAmount = true, + hasWhitespace = false, + decimalDigits = 2, + decimalSeparator = '.', + groupingSeparator = ',', + ), + CurrencyFormatElements.of(Locale("en-IE")) + ) + } + + @Test fun `of Japan Yen`() { + assertEquals( + CurrencyFormatElements( + symbol = "¥", + isSymbolBeforeAmount = true, + hasWhitespace = false, + decimalDigits = 0, + decimalSeparator = null, + groupingSeparator = ',', + ), + CurrencyFormatElements.of(Locale("ja-JP")) + ) + } + + @Test fun `of US Dollar`() { + assertEquals( + CurrencyFormatElements( + symbol = "$", + isSymbolBeforeAmount = true, + hasWhitespace = false, + decimalDigits = 2, + decimalSeparator = '.', + groupingSeparator = ',', + ), + CurrencyFormatElements.of(Locale("en-US")) + ) + } + + @Test fun `of Norway Krona`() { + assertEquals( + CurrencyFormatElements( + symbol = "kr", + isSymbolBeforeAmount = true, + hasWhitespace = true, + decimalDigits = 2, + decimalSeparator = ',', + groupingSeparator = '\u00A0', + ), + CurrencyFormatElements.of(Locale("nb-NO")) + ) + } +} diff --git a/app/src/iosMain/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatter.ios.kt b/app/src/iosMain/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatter.ios.kt new file mode 100644 index 00000000000..78a41d971b2 --- /dev/null +++ b/app/src/iosMain/kotlin/de/westnordost/streetcomplete/util/locale/CurrencyFormatter.ios.kt @@ -0,0 +1,20 @@ +package de.westnordost.streetcomplete.util.locale + +import androidx.compose.ui.text.intl.Locale +import platform.Foundation.NSLocaleCurrencyCode +import platform.Foundation.NSNumber +import platform.Foundation.NSNumberFormatter +import platform.Foundation.NSNumberFormatterCurrencyStyle + +actual class CurrencyFormatter actual constructor(locale: Locale?) { + + private val formatter = NSNumberFormatter().also { + if (locale != null) it.locale = locale.platformLocale + it.numberStyle = NSNumberFormatterCurrencyStyle + } + + actual fun format(value: Double): String = + formatter.stringFromNumber(NSNumber(value)) ?: "" + + actual val currencyCode: String? get() = formatter.currencyCode +} diff --git a/res/graphics/quest/parking_charge.svg b/res/graphics/quest/parking_charge.svg new file mode 100644 index 00000000000..9fbb80df368 --- /dev/null +++ b/res/graphics/quest/parking_charge.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + +