From ec4d60897aaf10821223ebee74b0864044cb7e5d Mon Sep 17 00:00:00 2001 From: soraefir Date: Mon, 29 Jun 2026 21:30:35 +0200 Subject: [PATCH] Currency parsing and formating # Conflicts: # app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt --- .../android/bill_edit/EditBillActivity.kt | 7 +- .../android/bill_edit/EditBillScreen.kt | 6 +- .../android/bill_edit/EditBillViewModel.kt | 10 +-- .../currencies/ManageCurrenciesScreen.kt | 7 +- .../cowspent/android/helper/FormatUtils.kt | 70 +++++++++++++++++-- .../android/main/BillsListComponents.kt | 1 + 6 files changed, 83 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt index a088640..fa6af65 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt @@ -187,7 +187,12 @@ class EditBillActivity : AppCompatActivity() { } } calendar.timeInMillis = bill.timestamp * 1000 - val members = db.getMembersOfProject(bill.projectId, null) + val allMembers = db.getMembersOfProject(bill.projectId, null) + val billOwerIds = bill.billOwersIds + val members = allMembers.filter { m -> + m.isActivated || m.id == bill.payerId || billOwerIds.contains(m.id) + } + val project = db.getProject(bill.projectId) val currencies = db.getCurrencies(bill.projectId) withContext(Dispatchers.Main) { diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt index bccaa90..3cf7784 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt @@ -188,7 +188,9 @@ fun BillBasicInfoSection( placeholder = { Text("0") }, modifier = Modifier.fillMaxWidth(), leadingIcon = { - val currencyToShow = viewModel.selectedCurrencyName.ifEmpty { viewModel.mainCurrencyName } + val currencyToShow = viewModel.selectedCurrencyName.ifEmpty { + viewModel.mainCurrencyName.ifEmpty { "$" } + } TextIconDisplay( textIcon = TextIcon.Symbol(currencyToShow), tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f) @@ -198,7 +200,7 @@ fun BillBasicInfoSection( IconButton( enabled = canEdit, onClick = { - val mainLabel = viewModel.mainCurrencyName.ifEmpty { "Main" } + val mainLabel = viewModel.mainCurrencyName.ifEmpty { "$" } val options = listOf("$mainLabel | Base") + viewModel.currencies.map { "${it.name} | 1 $mainLabel = ${it.exchangeRate} ${it.name}" } diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt index defdb8c..14545ec 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBMember import net.helcel.cowspent.android.helper.DialogState +import net.helcel.cowspent.android.helper.parseAmount import net.helcel.cowspent.util.SupportUtil import net.helcel.cowspent.util.evalMath @@ -41,13 +42,8 @@ class EditBillViewModel : ViewModel() { val amountAsDouble: Double get() { - val amountStr = amount.replace(',', '.') - return try { - if (amountStr.matches("[0-9.]+".toRegex())) { - amountStr.toDouble() - } else { - evalMath(amountStr) - } + return parseAmount(amount) ?: try { + evalMath(amount.replace(',', '.')) } catch (_: Exception) { 0.0 } diff --git a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt index abf976d..ef2261c 100644 --- a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import net.helcel.cowspent.R import net.helcel.cowspent.android.helper.AlertDialog +import net.helcel.cowspent.android.helper.formatAmount import net.helcel.cowspent.model.DBCurrency @Composable @@ -108,7 +109,7 @@ fun ManageCurrenciesScreen( horizontalArrangement = Arrangement.Center ) { Text("1 ", style = MaterialTheme.typography.body1) - Text(viewModel.mainCurrencyName.ifEmpty { "Base" }, fontWeight = FontWeight.Bold) + Text(viewModel.mainCurrencyName.ifEmpty { "$" }, fontWeight = FontWeight.Bold) Text(" = ", style = MaterialTheme.typography.h6) Text(viewModel.newCurrencyRate.ifEmpty { "0.0" }, fontWeight = FontWeight.Bold, color = MaterialTheme.colors.primary) Text(" ") @@ -170,7 +171,7 @@ fun CurrencyRow(currency: DBCurrency, mainCurrencyName: String, onDelete: () -> color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f) ) Text( - text = mainCurrencyName, + text = mainCurrencyName.ifEmpty { "$" }, style = MaterialTheme.typography.body1, fontWeight = FontWeight.Bold ) @@ -182,7 +183,7 @@ fun CurrencyRow(currency: DBCurrency, mainCurrencyName: String, onDelete: () -> ) Column { Text( - text = currency.exchangeRate.toString(), + text = formatAmount(currency.exchangeRate), style = MaterialTheme.typography.body1, fontWeight = FontWeight.ExtraBold ) diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/FormatUtils.kt b/app/src/main/java/net/helcel/cowspent/android/helper/FormatUtils.kt index ae57271..9d46c80 100644 --- a/app/src/main/java/net/helcel/cowspent/android/helper/FormatUtils.kt +++ b/app/src/main/java/net/helcel/cowspent/android/helper/FormatUtils.kt @@ -1,20 +1,80 @@ package net.helcel.cowspent.android.helper -import net.helcel.cowspent.util.SupportUtil +import java.text.NumberFormat import java.util.Locale import kotlin.math.abs import kotlin.math.round +/** + * Formats amount using k/M notation (e.g., 1.5k for 1500). + */ fun formatShortValue(value: Double): String { + val absValue = abs(value) + val sign = if (value < 0) "-" else "" return when { - value >= 1_000_000 -> String.format(Locale.ROOT, "%.1fM", value / 1_000_000).replace(".0", "") - value >= 1_000 -> String.format(Locale.ROOT, "%.1fk", value / 1_000).replace(".0", "") - else -> String.format(Locale.ROOT, "%.0f", value) + absValue >= 1_000_000 -> sign + formatAmount(absValue / 1_000_000) + "M" + absValue >= 1_000 -> sign + formatAmount(absValue / 1_000) + "k" + else -> sign + formatAmount(absValue) } } +/** + * Formats balance with sign and locale-aware number formatting. + */ fun formatBalance(balance: Double): String { val rbalance = round(abs(balance) * 100.0) / 100.0 val balanceSign = if (balance > 0.01) "+" else if (balance < -0.01) "-" else "" - return if (rbalance == 0.0) "" else "$balanceSign${SupportUtil.normalNumberFormat.format(rbalance)}" + return if (rbalance == 0.0) "" else "$balanceSign${formatAmount(rbalance)}" +} + +/** + * Formats amount using system locale or specified locale. + */ +fun formatAmount(value: Double, locale: Locale = Locale.getDefault()): String { + val formatter = NumberFormat.getNumberInstance(locale) + formatter.maximumFractionDigits = 2 + formatter.minimumFractionDigits = 0 + return formatter.format(value) +} + +/** + * Parses amount from string robustly. + * Tries common separators and cleans up non-numeric characters (except separators). + */ +fun parseAmount(input: String?): Double? { + if (input.isNullOrBlank()) return null + + // Remove non-numeric characters except for delimiters and minus sign + val cleaned = input.replace(Regex("[^0-9,.-]"), "").trim() + + if (cleaned.isEmpty()) return null + + // If there's both a comma and a dot, we assume the last one is the decimal separator + val lastDot = cleaned.lastIndexOf('.') + val lastComma = cleaned.lastIndexOf(',') + + return if (lastDot != -1 && lastComma != -1) { + if (lastDot > lastComma) { + // Dot is decimal, comma is thousands + cleaned.replace(",", "").toDoubleOrNull() + } else { + // Comma is decimal, dot is thousands + cleaned.replace(".", "").replace(",", ".").toDoubleOrNull() + } + } else if (lastComma != -1) { + // Only comma. Could be decimal or thousands. + if (cleaned.count { it == ',' } == 1) { + cleaned.replace(',', '.').toDoubleOrNull() + } else { + // Multiple commas -> thousands + cleaned.replace(",", "").toDoubleOrNull() + } + } else { + // Only dots or no separators + if (cleaned.count { it == '.' } > 1) { + cleaned.replace(".", "").toDoubleOrNull() + } else { + cleaned.toDoubleOrNull() + } + } } diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt index 8c20bc1..0d30465 100644 --- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import net.helcel.cowspent.R +import net.helcel.cowspent.android.helper.formatAmount import net.helcel.cowspent.android.helper.MemberAvatar import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBMember