Created
September 13, 2022 08:25
-
-
Save alashow/ab36170ce86ffe8399ff4a9a247abdcf to your computer and use it in GitHub Desktop.
Buggy CurrencyAmountMask
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package compose.CurrencyAmountMask | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.text.BasicTextField | |
import androidx.compose.foundation.text.KeyboardOptions | |
import androidx.compose.material3.LocalTextStyle | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.text.AnnotatedString | |
import androidx.compose.ui.text.input.KeyboardType | |
import androidx.compose.ui.text.input.OffsetMapping | |
import androidx.compose.ui.text.input.TransformedText | |
import androidx.compose.ui.text.input.VisualTransformation | |
import androidx.compose.ui.text.style.TextAlign | |
import androidx.compose.ui.tooling.preview.Preview | |
import androidx.compose.ui.unit.sp | |
import compose.CurrencyAmountMask.CurrencyUtils.toFormattedAmount | |
import java.text.DecimalFormat | |
import java.util.Currency | |
import kotlin.math.pow | |
class CurrencyAmountMask(private val currency: String) : VisualTransformation { | |
override fun filter(text: AnnotatedString): TransformedText { | |
val transformed = transform(text) | |
return transformed.toTransformedText() | |
} | |
private fun transform(value: AnnotatedString): CurrencyAmountTransformation { | |
val amount = value.text.toSafeLong() | |
val formatted = amount.toFormattedAmount(currency) | |
val digitPositions = mutableListOf<Int>() | |
val formattedOffsetMap = mutableListOf<Int>() | |
var nonDigitsCount = 0 | |
formatted.forEachIndexed { index, char -> | |
if (!char.isDigit()) { | |
nonDigitsCount++ | |
} else { | |
digitPositions.add(index) | |
} | |
formattedOffsetMap.add(index - nonDigitsCount) | |
} | |
digitPositions.add(digitPositions.maxOrNull()?.plus(1) ?: 0) | |
formattedOffsetMap.add(formattedOffsetMap.maxOrNull()?.plus(1) ?: 0) | |
return CurrencyAmountTransformation( | |
formatted, | |
digitPositions, | |
formattedOffsetMap | |
) | |
} | |
private data class CurrencyAmountTransformation( | |
private val formatted: String, | |
private val digitPositions: List<Int>, | |
private val formattedOffsetMap: List<Int> | |
) { | |
fun toTransformedText(): TransformedText { | |
return TransformedText( | |
text = AnnotatedString(formatted), | |
offsetMapping = object : OffsetMapping { | |
override fun originalToTransformed(offset: Int): Int { | |
return when (offset) { | |
0 -> 1 | |
else -> digitPositions.getOrNull(offset) ?: 0 | |
} | |
} | |
override fun transformedToOriginal(offset: Int): Int { | |
return when (offset) { | |
0 -> 1 | |
else -> formattedOffsetMap.getOrNull(offset) ?: 0 | |
} | |
} | |
} | |
) | |
} | |
} | |
} | |
@Preview | |
@Composable | |
fun CurrencyAmountMaskPreview() { | |
var amount by remember { mutableStateOf("1234") } | |
val mask = CurrencyAmountMask("USD") | |
Box(Modifier.fillMaxSize()) { | |
BasicTextField( | |
value = amount, | |
onValueChange = { | |
amount = it.toSafeLong().toString() | |
}, | |
visualTransformation = mask, | |
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center, fontSize = 48.sp), | |
modifier = Modifier | |
.fillMaxWidth() | |
.align(Alignment.Center), | |
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) | |
) | |
} | |
} | |
// Utils | |
fun String.toLongOrDefault(defaultValue: Long): Long = try { | |
toLong() | |
} catch (_: NumberFormatException) { | |
defaultValue | |
} | |
fun String?.toSafeLong(fallback: Long = 0): Long = when { | |
this == null -> fallback | |
else -> toLongOrDefault(fallback) | |
} | |
object CurrencyUtils { | |
fun getCurrencyFromCode(code: String?): Currency { | |
return Currency.getInstance(code) | |
} | |
private fun getCurrencyFormatInstance(currency: Currency): DecimalFormat { | |
val format: DecimalFormat = DecimalFormat.getCurrencyInstance() as DecimalFormat | |
if (format.currency != currency) { | |
format.currency = currency | |
} | |
return format | |
} | |
fun longToAmountString(currency: Currency, amount: Long): String { | |
val decimalFormat = getCurrencyFormatInstance(currency).apply { | |
minimumFractionDigits = currency.defaultFractionDigits | |
maximumFractionDigits = currency.defaultFractionDigits | |
} | |
return longToAmountString(currency, amount, decimalFormat) | |
} | |
fun Long.toFormattedAmount(merchantCurrency: String): String { | |
return longToAmountString(getCurrencyFromCode(merchantCurrency), this) | |
} | |
private fun longToAmountString( | |
currency: Currency, | |
amount: Long, | |
decimalFormat: DecimalFormat | |
): String { | |
return decimalFormat.format(longToDecimal(amount.toDouble(), currency)) | |
} | |
private fun longToDecimal(num: Double, currency: Currency): Double { | |
return num / 10.0.pow(currency.defaultFractionDigits.toDouble()) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment