Skip to content

Instantly share code, notes, and snippets.

@alashow
Created September 13, 2022 08:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alashow/ab36170ce86ffe8399ff4a9a247abdcf to your computer and use it in GitHub Desktop.
Save alashow/ab36170ce86ffe8399ff4a9a247abdcf to your computer and use it in GitHub Desktop.
Buggy CurrencyAmountMask
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