Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-18 12:20:15 +03:00
parent 3183f5b955
commit 7ca9946f47
53 changed files with 355 additions and 462 deletions

View file

@ -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() },
)
}
},

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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))
}
}