Currency parsing and formating
# Conflicts: # app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt
This commit is contained in:
@@ -187,7 +187,12 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
calendar.timeInMillis = bill.timestamp * 1000
|
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 project = db.getProject(bill.projectId)
|
||||||
val currencies = db.getCurrencies(bill.projectId)
|
val currencies = db.getCurrencies(bill.projectId)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|||||||
@@ -188,7 +188,9 @@ fun BillBasicInfoSection(
|
|||||||
placeholder = { Text("0") },
|
placeholder = { Text("0") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
val currencyToShow = viewModel.selectedCurrencyName.ifEmpty { viewModel.mainCurrencyName }
|
val currencyToShow = viewModel.selectedCurrencyName.ifEmpty {
|
||||||
|
viewModel.mainCurrencyName.ifEmpty { "$" }
|
||||||
|
}
|
||||||
TextIconDisplay(
|
TextIconDisplay(
|
||||||
textIcon = TextIcon.Symbol(currencyToShow),
|
textIcon = TextIcon.Symbol(currencyToShow),
|
||||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||||
@@ -198,7 +200,7 @@ fun BillBasicInfoSection(
|
|||||||
IconButton(
|
IconButton(
|
||||||
enabled = canEdit,
|
enabled = canEdit,
|
||||||
onClick = {
|
onClick = {
|
||||||
val mainLabel = viewModel.mainCurrencyName.ifEmpty { "Main" }
|
val mainLabel = viewModel.mainCurrencyName.ifEmpty { "$" }
|
||||||
val options = listOf("$mainLabel | Base") + viewModel.currencies.map {
|
val options = listOf("$mainLabel | Base") + viewModel.currencies.map {
|
||||||
"${it.name} | 1 $mainLabel = ${it.exchangeRate} ${it.name}"
|
"${it.name} | 1 $mainLabel = ${it.exchangeRate} ${it.name}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import net.helcel.cowspent.model.DBBill
|
import net.helcel.cowspent.model.DBBill
|
||||||
import net.helcel.cowspent.model.DBMember
|
import net.helcel.cowspent.model.DBMember
|
||||||
import net.helcel.cowspent.android.helper.DialogState
|
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.SupportUtil
|
||||||
import net.helcel.cowspent.util.evalMath
|
import net.helcel.cowspent.util.evalMath
|
||||||
|
|
||||||
@@ -41,13 +42,8 @@ class EditBillViewModel : ViewModel() {
|
|||||||
|
|
||||||
val amountAsDouble: Double
|
val amountAsDouble: Double
|
||||||
get() {
|
get() {
|
||||||
val amountStr = amount.replace(',', '.')
|
return parseAmount(amount) ?: try {
|
||||||
return try {
|
evalMath(amount.replace(',', '.'))
|
||||||
if (amountStr.matches("[0-9.]+".toRegex())) {
|
|
||||||
amountStr.toDouble()
|
|
||||||
} else {
|
|
||||||
evalMath(amountStr)
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
0.0
|
0.0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import net.helcel.cowspent.R
|
import net.helcel.cowspent.R
|
||||||
import net.helcel.cowspent.android.helper.AlertDialog
|
import net.helcel.cowspent.android.helper.AlertDialog
|
||||||
|
import net.helcel.cowspent.android.helper.formatAmount
|
||||||
import net.helcel.cowspent.model.DBCurrency
|
import net.helcel.cowspent.model.DBCurrency
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -108,7 +109,7 @@ fun ManageCurrenciesScreen(
|
|||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text("1 ", style = MaterialTheme.typography.body1)
|
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(" = ", style = MaterialTheme.typography.h6)
|
||||||
Text(viewModel.newCurrencyRate.ifEmpty { "0.0" }, fontWeight = FontWeight.Bold, color = MaterialTheme.colors.primary)
|
Text(viewModel.newCurrencyRate.ifEmpty { "0.0" }, fontWeight = FontWeight.Bold, color = MaterialTheme.colors.primary)
|
||||||
Text(" ")
|
Text(" ")
|
||||||
@@ -170,7 +171,7 @@ fun CurrencyRow(currency: DBCurrency, mainCurrencyName: String, onDelete: () ->
|
|||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = mainCurrencyName,
|
text = mainCurrencyName.ifEmpty { "$" },
|
||||||
style = MaterialTheme.typography.body1,
|
style = MaterialTheme.typography.body1,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
@@ -182,7 +183,7 @@ fun CurrencyRow(currency: DBCurrency, mainCurrencyName: String, onDelete: () ->
|
|||||||
)
|
)
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = currency.exchangeRate.toString(),
|
text = formatAmount(currency.exchangeRate),
|
||||||
style = MaterialTheme.typography.body1,
|
style = MaterialTheme.typography.body1,
|
||||||
fontWeight = FontWeight.ExtraBold
|
fontWeight = FontWeight.ExtraBold
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,20 +1,80 @@
|
|||||||
package net.helcel.cowspent.android.helper
|
package net.helcel.cowspent.android.helper
|
||||||
|
|
||||||
import net.helcel.cowspent.util.SupportUtil
|
import java.text.NumberFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.round
|
import kotlin.math.round
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats amount using k/M notation (e.g., 1.5k for 1500).
|
||||||
|
*/
|
||||||
fun formatShortValue(value: Double): String {
|
fun formatShortValue(value: Double): String {
|
||||||
|
val absValue = abs(value)
|
||||||
|
val sign = if (value < 0) "-" else ""
|
||||||
return when {
|
return when {
|
||||||
value >= 1_000_000 -> String.format(Locale.ROOT, "%.1fM", value / 1_000_000).replace(".0", "")
|
absValue >= 1_000_000 -> sign + formatAmount(absValue / 1_000_000) + "M"
|
||||||
value >= 1_000 -> String.format(Locale.ROOT, "%.1fk", value / 1_000).replace(".0", "")
|
absValue >= 1_000 -> sign + formatAmount(absValue / 1_000) + "k"
|
||||||
else -> String.format(Locale.ROOT, "%.0f", value)
|
else -> sign + formatAmount(absValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats balance with sign and locale-aware number formatting.
|
||||||
|
*/
|
||||||
fun formatBalance(balance: Double): String {
|
fun formatBalance(balance: Double): String {
|
||||||
val rbalance = round(abs(balance) * 100.0) / 100.0
|
val rbalance = round(abs(balance) * 100.0) / 100.0
|
||||||
val balanceSign = if (balance > 0.01) "+" else if (balance < -0.01) "-" else ""
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import net.helcel.cowspent.R
|
import net.helcel.cowspent.R
|
||||||
|
import net.helcel.cowspent.android.helper.formatAmount
|
||||||
import net.helcel.cowspent.android.helper.MemberAvatar
|
import net.helcel.cowspent.android.helper.MemberAvatar
|
||||||
import net.helcel.cowspent.model.DBBill
|
import net.helcel.cowspent.model.DBBill
|
||||||
import net.helcel.cowspent.model.DBMember
|
import net.helcel.cowspent.model.DBMember
|
||||||
|
|||||||
Reference in New Issue
Block a user