diff --git a/app/src/main/java/cash/z/ecc/android/ext/CurrencyFormatter.kt b/app/src/main/java/cash/z/ecc/android/ext/CurrencyFormatter.kt new file mode 100644 index 0000000..c29ed82 --- /dev/null +++ b/app/src/main/java/cash/z/ecc/android/ext/CurrencyFormatter.kt @@ -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()) +} \ No newline at end of file diff --git a/app/src/main/java/cash/z/ecc/android/ext/EditText.kt b/app/src/main/java/cash/z/ecc/android/ext/EditText.kt index adba13b..74d0408 100644 --- a/app/src/main/java/cash/z/ecc/android/ext/EditText.kt +++ b/app/src/main/java/cash/z/ecc/android/ext/EditText.kt @@ -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 { diff --git a/app/src/main/java/cash/z/ecc/android/ui/home/HomeViewModel.kt b/app/src/main/java/cash/z/ecc/android/ui/home/HomeViewModel.kt index 5e0dfec..6658a85 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/home/HomeViewModel.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/home/HomeViewModel.kt @@ -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" } } diff --git a/app/src/main/java/cash/z/ecc/android/ui/send/SendAddressFragment.kt b/app/src/main/java/cash/z/ecc/android/ui/send/SendAddressFragment.kt index 09c41ba..aff9d12 100644 --- a/app/src/main/java/cash/z/ecc/android/ui/send/SendAddressFragment.kt +++ b/app/src/main/java/cash/z/ecc/android/ui/send/SendAddressFragment.kt @@ -52,7 +52,7 @@ class SendAddressFragment : BaseFragment(), // 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(), 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()