Updated on 2026-08-14
This commit is contained in:
parent
3183f5b955
commit
7ca9946f47
53 changed files with 355 additions and 462 deletions
|
|
@ -24,9 +24,10 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.annotatedReference
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
|
|
@ -98,7 +99,7 @@ private fun InputRowImageSelectorPreview(
|
|||
append(" ")
|
||||
withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) {
|
||||
append(
|
||||
BigDecimalFormatter.formatPercent(BigDecimal.ZERO, true),
|
||||
BigDecimal.ZERO.format { percent() },
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -120,6 +120,26 @@ fun BigDecimalCryptoFormat.uncapped() = BigDecimalFormat { value ->
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format for displaying crypto amounts with a fixed number of decimals.
|
||||
*/
|
||||
fun BigDecimalCryptoFormat.anyDecimals(maxDecimals: Int = decimals, minDecimals: Int = decimals) =
|
||||
BigDecimalFormat { value ->
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = maxDecimals
|
||||
minimumFractionDigits = minDecimals
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
formatter.format(value)
|
||||
.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.getSymbol(locale),
|
||||
cryptoCurrencySymbol = symbol,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format for displaying fees.
|
||||
* If the fee is less than the threshold, it will be displayed as a fixed value "<0.000001 BTC", "<BTC 0.000001".
|
||||
|
|
@ -185,6 +205,11 @@ internal fun String.replaceFiatSymbolWithCrypto(
|
|||
when {
|
||||
str.endsWith(fiatCurrencySymbol) -> {
|
||||
val withoutSymbol = str.dropLast(fiatCurrencySymbol.length)
|
||||
|
||||
if (cryptoCurrencySymbol.isBlank()) {
|
||||
return withoutSymbol
|
||||
}
|
||||
|
||||
val last = withoutSymbol.lastOrNull() ?: return cryptoCurrencySymbol
|
||||
|
||||
append(withoutSymbol)
|
||||
|
|
@ -200,12 +225,16 @@ internal fun String.replaceFiatSymbolWithCrypto(
|
|||
append(CURRENCY_SPACE)
|
||||
}
|
||||
|
||||
append(cryptoCurrencySymbol)
|
||||
|
||||
val withoutSymbol = str.drop(fiatCurrencySymbol.length)
|
||||
val first = withoutSymbol.firstOrNull()
|
||||
?: return cryptoCurrencySymbol
|
||||
|
||||
if (cryptoCurrencySymbol.isBlank()) {
|
||||
return withoutSymbol
|
||||
}
|
||||
|
||||
append(cryptoCurrencySymbol)
|
||||
|
||||
if (first.isNotWhitespace()) {
|
||||
append(CURRENCY_SPACE)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
open class BigDecimalSimpleFormat(
|
||||
val decimals: Int,
|
||||
val locale: Locale = Locale.getDefault(),
|
||||
) : BigDecimalFormat {
|
||||
|
||||
override fun invoke(value: BigDecimal): String = default()(value)
|
||||
}
|
||||
|
||||
// == Initializers ==
|
||||
|
||||
fun BigDecimalFormatScope.simple(decimals: Int, locale: Locale = Locale.getDefault()) = BigDecimalSimpleFormat(
|
||||
decimals = decimals,
|
||||
locale = locale,
|
||||
)
|
||||
|
||||
// == Formatters ==
|
||||
|
||||
fun BigDecimalSimpleFormat.default() = BigDecimalFormat { value ->
|
||||
val formatter = NumberFormat.getInstance(locale).apply {
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = 0
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
formatter.format(value)
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import com.tangem.utils.StringsSigns.LOWER_SIGN
|
||||
import com.tangem.utils.StringsSigns.TILDE_SIGN
|
||||
|
|
@ -16,10 +15,8 @@ object BigDecimalFormatter {
|
|||
|
||||
const val EMPTY_BALANCE_SIGN = DASH_SIGN
|
||||
private const val CAN_BE_LOWER_SIGN = LOWER_SIGN
|
||||
private val FORMAT_THRESHOLD = BigDecimal("0.01")
|
||||
|
||||
private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01")
|
||||
private val CRYPTO_FEE_FORMAT_THRESHOLD = BigDecimal("0.000001")
|
||||
|
||||
private const val FIAT_MARKET_DEFAULT_DIGITS = 2
|
||||
private const val FIAT_MARKET_EXTENDED_DIGITS = 6
|
||||
|
|
@ -27,138 +24,6 @@ object BigDecimalFormatter {
|
|||
|
||||
private val usdCurrency = Currency.getInstance("USD")
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatCryptoAmount(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: String,
|
||||
decimals: Int,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = NumberFormat.getNumberInstance(locale).apply {
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount).let {
|
||||
if (cryptoCurrency.isEmpty()) {
|
||||
it
|
||||
} else {
|
||||
it + "\u2009$cryptoCurrency"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatCryptoAmountShorted(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: String,
|
||||
decimals: Int,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = if (cryptoAmount.isMoreThanThreshold()) {
|
||||
NumberFormat.getNumberInstance(locale).apply {
|
||||
maximumFractionDigits = 2
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
} else {
|
||||
NumberFormat.getNumberInstance(locale).apply {
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.DOWN
|
||||
}
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount).let {
|
||||
if (cryptoCurrency.isEmpty()) {
|
||||
it
|
||||
} else {
|
||||
it + "\u2009$cryptoCurrency"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatCryptoAmountUncapped(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = NumberFormat.getNumberInstance(locale).apply {
|
||||
maximumFractionDigits = cryptoCurrency.decimals
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount).let {
|
||||
if (cryptoCurrency.symbol.isEmpty()) {
|
||||
it
|
||||
} else {
|
||||
it + "\u2009${cryptoCurrency.symbol}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatCryptoFeeAmount(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: String,
|
||||
decimals: Int,
|
||||
canBeLower: Boolean = false,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = NumberFormat.getNumberInstance(locale).apply {
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
val amountFormatted = if (cryptoAmount.checkCryptoThreshold()) {
|
||||
buildString {
|
||||
append(CAN_BE_LOWER_SIGN)
|
||||
append(
|
||||
formatter.format(CRYPTO_FEE_FORMAT_THRESHOLD),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
buildString {
|
||||
if (canBeLower) {
|
||||
append(CAN_BE_LOWER_SIGN)
|
||||
}
|
||||
append(formatter.format(cryptoAmount))
|
||||
}
|
||||
}
|
||||
|
||||
return if (cryptoCurrency.isEmpty()) {
|
||||
amountFormatted
|
||||
} else {
|
||||
amountFormatted + "\u2009$cryptoCurrency"
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatCryptoAmount(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals, locale)
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatFiatAmount(
|
||||
fiatAmount: BigDecimal?,
|
||||
|
|
@ -266,28 +131,6 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatPercent(
|
||||
percent: BigDecimal,
|
||||
useAbsoluteValue: Boolean,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
maxFractionDigits: Int = 2,
|
||||
minFractionDigits: Int = 2,
|
||||
): String {
|
||||
val formatter = NumberFormat.getPercentInstance(locale).apply {
|
||||
maximumFractionDigits = maxFractionDigits
|
||||
minimumFractionDigits = minFractionDigits
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
val value = if (useAbsoluteValue) percent.abs() else percent
|
||||
|
||||
return formatter.format(value)
|
||||
}
|
||||
|
||||
fun formatWithSymbol(amount: String, symbol: String) = "$amount\u2009$symbol"
|
||||
|
||||
private fun BigDecimal.isMoreThanThreshold() = this > FORMAT_THRESHOLD
|
||||
|
||||
private fun getCurrency(code: String): Currency {
|
||||
return runCatching { Currency.getInstance(code) }
|
||||
.getOrElse { e ->
|
||||
|
|
@ -301,6 +144,4 @@ object BigDecimalFormatter {
|
|||
}
|
||||
|
||||
private fun BigDecimal.checkFiatThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD
|
||||
|
||||
private fun BigDecimal.checkCryptoThreshold() = this > BigDecimal.ZERO && this < CRYPTO_FEE_FORMAT_THRESHOLD
|
||||
}
|
||||
|
|
@ -370,4 +370,38 @@ internal class BigDecimalCryptoFormatTest {
|
|||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.13".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
// === anyDecimals() ===
|
||||
|
||||
@Test
|
||||
fun `anyDecimals smoke`() {
|
||||
val testValue = BigDecimal("0.123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 5,
|
||||
locale = testLocale,
|
||||
).anyDecimals()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.12341".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `anyDecimals zero`() {
|
||||
val testValue = BigDecimal("0.123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 0,
|
||||
locale = testLocale,
|
||||
).anyDecimals()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue