Merge pull request #177 from CrystalPony/master

Limits decimal places in amount inputs on the home and send screens
This commit is contained in:
Kevin Gorham 2020-07-02 18:05:08 -04:00 committed by GitHub
commit 1569d770bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 197 additions and 3 deletions

View File

@ -0,0 +1,133 @@
package cash.z.ecc.android.ext
import cash.z.ecc.android.sdk.ext.Conversions
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.convertZatoshiToZec
import cash.z.ecc.android.sdk.ext.toZec
import java.math.BigDecimal
import java.math.MathContext
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.*
object ConversionsUniform {
var ONE_ZEC_IN_ZATOSHI = BigDecimal(ZcashSdk.ZATOSHI_PER_ZEC, MathContext.DECIMAL128)
var ZEC_FORMATTER = (NumberFormat.getNumberInstance(Locale("en", "UK")) as DecimalFormat).apply {
applyPattern("###.##")
roundingMode = RoundingMode.DOWN
maximumFractionDigits = 6
minimumFractionDigits = 0
minimumIntegerDigits = 1
}
}
/**
* Format a Zatoshi value into ZEC with the given number of digits, represented as a string.
* Start with Zatoshi -> End with ZEC.
*
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
* better than USD.
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
*
* @return this Zatoshi value represented as ZEC, in a string with at least [minDecimals] and at
* most [maxDecimals]
*/
inline fun Long?.convertZatoshiToZecStringUniform(
maxDecimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits,
minDecimals: Int = ConversionsUniform.ZEC_FORMATTER.minimumFractionDigits
): String {
return currencyFormatterUniform(maxDecimals, minDecimals).format(this.convertZatoshiToZec(maxDecimals))
}
/**
* Format a ZEC value into ZEC with the given number of digits, represented as a string.
* Start with ZEC -> End with ZEC.
*
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
* better when right.
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
*
* @return this Double ZEC value represented as a string with at least [minDecimals] and at most
* [maxDecimals].
*/
inline fun Double?.toZecStringUniform(
maxDecimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits,
minDecimals: Int = ConversionsUniform.ZEC_FORMATTER.minimumFractionDigits
): String {
return currencyFormatterUniform(maxDecimals, minDecimals).format(this.toZec(maxDecimals))
}
/**
* Format a Zatoshi value into ZEC with the given number of decimal places, represented as a string.
* Start with ZeC -> End with ZEC.
*
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
* better than bread.
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
*
* @return this BigDecimal ZEC value represented as a string with at least [minDecimals] and at most
* [maxDecimals].
*/
inline fun BigDecimal?.toZecStringUniform(
maxDecimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits,
minDecimals: Int = ConversionsUniform.ZEC_FORMATTER.minimumFractionDigits
): String {
return currencyFormatterUniform(maxDecimals, minDecimals).format(this.toZecUniform(maxDecimals))
}
/**
* Format a Double ZEC value as a BigDecimal ZEC value, right-padded to the given number of fraction
* digits.
* Start with ZEC -> End with ZEC.
*
* @param decimals the scale to use for the resulting BigDecimal.
*
* @return this Double ZEC value converted into a BigDecimal, with the proper rounding mode for use
* with other formatting functions.
*/
inline fun Double?.toZec(decimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
return BigDecimal(this?.toString() ?: "0.0", MathContext.DECIMAL128).setScale(
decimals,
ConversionsUniform.ZEC_FORMATTER.roundingMode
)
}
/**
* Format a BigDecimal ZEC value as a BigDecimal ZEC value, right-padded to the given number of
* fraction digits.
* Start with ZEC -> End with ZEC.
*
* @param decimals the scale to use for the resulting BigDecimal.
*
* @return this BigDecimal ZEC adjusted to the default scale and rounding mode.
*/
inline fun BigDecimal?.toZecUniform(decimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
return (this ?: BigDecimal.ZERO).setScale(decimals, ConversionsUniform.ZEC_FORMATTER.roundingMode)
}
/**
* Create a number formatter for use with converting currency to strings. This probably isn't needed
* externally since the other formatting functions leverage this, instead. Leverages the default
* rounding mode for ZEC found in ZEC_FORMATTER.
*
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
* glorious.
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
*
* @return a currency formatter, appropriate for the default locale.
*/
inline fun currencyFormatterUniform(maxDecimals: Int, minDecimals: Int): DecimalFormat {
return (ConversionsUniform.ZEC_FORMATTER.clone() as DecimalFormat).apply {
maximumFractionDigits = maxDecimals
minimumFractionDigits = minDecimals
}
}
/**
* Checks if the decimal separator is the last symbol
*/
inline fun String.endsWithDecimalSeparator(): Boolean {
return this.endsWith(ConversionsUniform.ZEC_FORMATTER.decimalFormatSymbols.toString())
}

View File

@ -1,5 +1,7 @@
package cash.z.ecc.android.ext
import android.text.Editable
import android.text.TextWatcher
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.widget.EditText
import android.widget.TextView
@ -18,6 +20,59 @@ fun EditText.onEditorActionDone(block: (EditText) -> Unit) {
}
}
inline fun EditText.limitDecimalPlaces(max: Int) {
val editText = this
addTextChangedListener(object : TextWatcher {
var previousValue = ""
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// Cache the previous value
previousValue = text.toString()
}
override fun afterTextChanged(s: Editable?) {
var textStr = text.toString()
if (textStr.isNotEmpty()) {
val oldText = text.toString()
val number = textStr.safelyConvertToBigDecimal()
if (number != null && number.scale() > 8) {
// Prevent the user from adding a new decimal place somewhere in the middle if we're already at the limit
if (editText.selectionStart == editText.selectionEnd && editText.selectionStart != textStr.length) {
textStr = previousValue
} else {
textStr = number.toZecStringUniform(8)
}
}
// Trim leading zeroes
textStr = textStr.trimStart('0')
// Append a zero if this results in an empty string or if the first symbol is not a digit
if (textStr.isEmpty() || !textStr.first().isDigit()) {
textStr = "0$textStr"
}
// Restore the cursor position
if (oldText != textStr) {
val cursorPosition = editText.selectionEnd;
editText.setText(textStr)
editText.setSelection(
(cursorPosition - (oldText.length - textStr.length)).coerceIn(
0,
editText.text.toString().length
)
)
}
}
}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
})
}
fun TextView.convertZecToZatoshi(): Long? {
return try {

View File

@ -45,7 +45,7 @@ class HomeViewModel @Inject constructor() : ViewModel() {
|| (c == '<' && acc == "0")
|| (c == '.' && acc.contains('.')) -> {twig("triggered: 1 acc: $acc c: $c")
acc
}
}
c == '<' && acc.length <= 1 -> {twig("triggered: 2 $typedChars")
"0"
}
@ -55,7 +55,11 @@ class HomeViewModel @Inject constructor() : ViewModel() {
acc == "0" && c != '.' -> {twig("triggered: 4 $typedChars")
c.toString()
}
else -> {twig("triggered: 5 $typedChars")
acc.contains('.') && acc.length - acc.indexOf('.') > 8 -> {
twig("triggered: 5 $typedChars")
acc
}
else -> {twig("triggered: 6 $typedChars")
"$acc$c"
}
}

View File

@ -52,7 +52,7 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
// Apply View Model
if (sendViewModel.zatoshiAmount > 0L) {
sendViewModel.zatoshiAmount.convertZatoshiToZecString(8).let { amount ->
sendViewModel.zatoshiAmount.convertZatoshiToZecStringUniform(8).let { amount ->
binding.inputZcashAmount.setText(amount)
}
} else {
@ -67,6 +67,8 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
binding.inputZcashAddress.onEditorActionDone(::onSubmit).also { tapped(SEND_ADDRESS_DONE_ADDRESS) }
binding.inputZcashAmount.onEditorActionDone(::onSubmit).also { tapped(SEND_ADDRESS_DONE_AMOUNT) }
binding.inputZcashAmount.limitDecimalPlaces(8)
binding.inputZcashAddress.apply {
doAfterTextChanged {
val textStr = text.toString()