diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index e6944920a6..fb78b58f1e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -5,6 +5,7 @@ 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 +import com.tangem.utils.extensions.isNotWhitespace import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode @@ -20,8 +21,6 @@ object BigDecimalFormatter { private const val CAN_BE_LOWER_SIGN = LOWER_SIGN private val FORMAT_THRESHOLD = BigDecimal("0.01") - private const val TEMP_CURRENCY_CODE = "USD" - private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01") private val CRYPTO_FEE_FORMAT_THRESHOLD = BigDecimal("0.000001") @@ -29,6 +28,12 @@ object BigDecimalFormatter { private const val FIAT_MARKET_EXTENDED_DIGITS = 6 private const val FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES = 4 + private val usdCurrency = Currency.getInstance("USD") + + @Deprecated( + "Use formatCryptoAmount2", + replaceWith = ReplaceWith("formatCryptoAmount2"), + ) fun formatCryptoAmount( cryptoAmount: BigDecimal?, cryptoCurrency: String, @@ -53,6 +58,30 @@ object BigDecimalFormatter { } } + // Migrate to this method from formatCryptoAmount ([REDACTED_TASK_KEY]) + fun formatCryptoAmount2( + cryptoAmount: BigDecimal?, + cryptoCurrency: String, + decimals: Int, + locale: Locale = Locale.getDefault(), + ): String { + if (cryptoAmount == null) return EMPTY_BALANCE_SIGN + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + return formatter.format(cryptoAmount) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.symbol, + cryptoCurrencySymbol = cryptoCurrency, + ) + } + fun formatCryptoAmountShorted( cryptoAmount: BigDecimal?, cryptoCurrency: String, @@ -305,7 +334,7 @@ object BigDecimalFormatter { .getOrElse { e -> // Currency code is not valid ISO 4217 code if (e is IllegalArgumentException) { - Currency.getInstance(TEMP_CURRENCY_CODE) + usdCurrency } else { throw e } @@ -316,7 +345,7 @@ object BigDecimalFormatter { * Adds a proper currency sign for the provided formatted [amount] * ex. '10.0k" -> "$10.0k", "string" -> "$string" */ - fun addCurrencySymbolToStringAmount( + private fun addCurrencySymbolToStringAmount( amount: String, fiatCurrencyCode: String, fiatCurrencySymbol: String, @@ -338,6 +367,32 @@ object BigDecimalFormatter { return formatted } + /** + * Adds a proper currency sign for the provided formatted [amount] + * ex. '10.0k" -> "ETH 10.0k", "string" -> "ETH string" + */ + private fun addCryptoCurrencySymbolToStringAmount( + amount: String, + cryptoCurrencySymbol: String, + locale: Locale = Locale.getDefault(), + ): String { + val sampleAmount = BigDecimal.TEN + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + maximumFractionDigits = 0 + minimumFractionDigits = 0 + currency = usdCurrency + } + + val formatted = formatter.format(sampleAmount) + .replace(sampleAmount.toString(), amount) + + return formatted.replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.symbol, + cryptoCurrencySymbol = cryptoCurrencySymbol, + ) + } + /** * "123456.6" -> "$123.457K" * "12345.6" -> "$123.046K" @@ -380,6 +435,45 @@ object BigDecimalFormatter { ) } + /** + * "123456.6" -> "ETH 123.457K" + * "12345.6" -> "123.046K ETH" + * Negative amount is not supported + * @param threeDigitsMethod if true, will format the amount always with 3 significant digits + * @param scale the number of digits to the right of the decimal point + */ + fun formatCompactCryptoAmount( + amount: BigDecimal?, + cryptoCurrencySymbol: String, + threeDigitsMethod: Boolean = false, + decimals: Int = 0, + locale: Locale = Locale.getDefault(), + ): String { + if (amount == null) return EMPTY_BALANCE_SIGN + + if (amount < BigDecimal.ONE) { + return formatCryptoAmount2( + cryptoAmount = amount, + cryptoCurrency = cryptoCurrencySymbol, + decimals = decimals, + locale = locale, + ) + } + + val rawAmount = formatCompactAmount( + amount = amount, + locale = locale, + threeDigitsMethod = threeDigitsMethod, + scale = decimals, + ) + + return addCryptoCurrencySymbolToStringAmount( + amount = rawAmount, + cryptoCurrencySymbol = cryptoCurrencySymbol, + locale = locale, + ) + } + /** * "123456.6" -> "123.457K" * "12345.6" -> "123.046K" @@ -413,14 +507,61 @@ object BigDecimalFormatter { return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP)) } else { - val value = amount.setScale(scale, RoundingMode.HALF_UP) + val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP) + val digitsCount = scaledAmount.longValueExact().toString().count() + val digitsToFormat = 5 - when (digitsCount % 3) { + 0 -> 0 + 1 -> 2 + else -> 1 + } val formatter = CompactDecimalFormat.getInstance( locale, CompactDecimalFormat.CompactStyle.SHORT, - ) + ).apply { + minimumSignificantDigits = 2 + maximumSignificantDigits = digitsToFormat + } - return formatter.format(value) + return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP)) + } + } + + // Replaces fiat currency symbol with crypto currency symbol + // with respect to the position of the symbol and whitespace + private fun String.replaceFiatSymbolWithCrypto(fiatCurrencySymbol: String, cryptoCurrencySymbol: String): String { + val str = this + if (str.isEmpty()) return str + + return buildString { + when { + str.endsWith(fiatCurrencySymbol) -> { + val withoutSymbol = str.dropLast(fiatCurrencySymbol.length) + val last = withoutSymbol.lastOrNull() ?: return cryptoCurrencySymbol + + append(withoutSymbol) + + if (last.isNotWhitespace()) { + append("\u2009") + } + + append(cryptoCurrencySymbol) + } + str.startsWith(fiatCurrencySymbol) -> { + append(cryptoCurrencySymbol) + + val withoutSymbol = str.drop(fiatCurrencySymbol.length) + val first = withoutSymbol.firstOrNull() + ?: return cryptoCurrencySymbol + + if (first.isNotWhitespace()) { + append("\u2009") + } + + append(withoutSymbol) + } + else -> append(str) + } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatterCompat.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatterCompat.kt deleted file mode 100644 index 9218188bef..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatterCompat.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.tangem.core.ui.utils - -import java.math.BigDecimal -import java.math.RoundingMode -import java.util.Locale - -internal object BigDecimalFormatterCompat { - - /** - * Formats value as [BigDecimalFormatter.formatCompactFiatAmount] does using only "T","B","M","K" suffixes - * Used for < API24 compatibility - */ - @Suppress("MagicNumber", "UnnecessaryParentheses") - fun formatCompactFiatAmountNoLocaleContext( - amount: BigDecimal, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - val formatted = formatCompactAmountNoLocaleContext(amount) - - return BigDecimalFormatter.addCurrencySymbolToStringAmount( - amount = formatted, - fiatCurrencyCode = fiatCurrencyCode, - fiatCurrencySymbol = fiatCurrencySymbol, - locale = locale, - ) - } - - /** - * Formats value as [BigDecimalFormatter.formatCompactAmount] does using only "T","B","M","K" suffixes - * Used for < API24 compatibility - */ - @Suppress("MagicNumber", "UnnecessaryParentheses") - fun formatCompactAmountNoLocaleContext(amount: BigDecimal): String { - val value = amount.setScale(0, RoundingMode.HALF_UP).longValueExact() - - val formatted = when { - value > 1_000_000_000_000L -> { - val trillion = value / 1_000_000_000_000 - val billion = (value % 1_000_000_000_000) / 1_000_000_000 - "$trillion.${billion}T" - } - value > 1_000_000_000L -> { - val billion = value / 1_000_000_000 - val million = (value % 1_000_000_000) / 1_000_000 - "$billion.${million}B" - } - value > 1_000_000L -> { - val million = value / 1_000_000 - val thousand = (value % 1_000_000) / 1_000 - "$million.${thousand}M" - } - value > 1_000L -> { - val thousand = value / 1_000 - "${thousand}K" - } - else -> return value.toString() - } - - return formatted - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt index 3501ff40f5..ea254cfd4c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt @@ -18,6 +18,7 @@ import java.math.BigDecimal @Stable internal class MetricsConverter( private val appCurrency: Provider, + private val tokenSymbol: String, private val onInfoClick: (InfoBottomSheetContent) -> Unit, ) : Converter { @@ -119,9 +120,13 @@ internal class MetricsConverter( if (this == null) return StringsSigns.DASH_SIGN return if (crypto) { - BigDecimalFormatter.formatCompactAmount(amount = this) + BigDecimalFormatter.formatCompactCryptoAmount( + amount = this, + cryptoCurrencySymbol = tokenSymbol, + ) } else { val currency = appCurrency() + BigDecimalFormatter.formatCompactFiatAmount( amount = this, fiatCurrencyCode = currency.code, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt index 4f3145408b..8a9b8aa82a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt @@ -12,8 +12,8 @@ import com.tangem.utils.converter.Converter @Stable internal class TokenMarketInfoConverter( - appCurrency: Provider, - onInfoClick: (InfoBottomSheetContent) -> Unit, + private val appCurrency: Provider, + private val onInfoClick: (InfoBottomSheetContent) -> Unit, onLinkClick: (LinksUM.Link) -> Unit, onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit, onInsightsIntervalChanged: (PriceChangeInterval) -> Unit, @@ -28,7 +28,6 @@ internal class TokenMarketInfoConverter( @Suppress("UnusedPrivateMember") // TODO second markets iteration private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick) - private val metricsConverter = MetricsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick) private val pricePerformanceConverter = PricePerformanceConverter( appCurrency = appCurrency, onIntervalChanged = onPricePerformanceIntervalChanged, @@ -36,6 +35,12 @@ internal class TokenMarketInfoConverter( private val linksConverter = LinksConverter(onLinkClick = onLinkClick) override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks { + val metricsConverter = MetricsConverter( + tokenSymbol = value.symbol, + appCurrency = appCurrency, + onInfoClick = onInfoClick, + ) + return MarketsTokenDetailsUM.InformationBlocks( insights = value.insights?.let { insightsConverter.convert(it) }, securityScore = null,