Updated on 2026-08-14
This commit is contained in:
parent
59daa44548
commit
6465307d0a
19 changed files with 1499 additions and 303 deletions
|
|
@ -1,3 +1,5 @@
|
|||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.kotlin.android) apply false
|
||||
alias(deps.plugins.kotlin.jvm) apply false
|
||||
|
|
@ -20,6 +22,16 @@ interface Injected {
|
|||
val fs: FileSystemOperations
|
||||
}
|
||||
|
||||
// Test Logging
|
||||
subprojects {
|
||||
tasks.withType<Test> {
|
||||
testLogging {
|
||||
exceptionFormat = TestExceptionFormat.FULL
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val assembleInternalQA by tasks.registering {
|
||||
group = "build"
|
||||
description = "Builds internal APK to 'build/outputs' directory"
|
||||
|
|
|
|||
|
|
@ -52,4 +52,9 @@ dependencies {
|
|||
api(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.markdown)
|
||||
|
||||
/** Tests */
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
}
|
||||
|
|
@ -4,10 +4,15 @@ import androidx.compose.ui.text.AnnotatedString
|
|||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE
|
||||
import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
|
||||
import com.tangem.core.ui.utils.defaultFormat
|
||||
import com.tangem.core.ui.utils.formatWithThousands
|
||||
import timber.log.Timber
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
class AmountVisualTransformation(
|
||||
private val decimals: Int,
|
||||
|
|
@ -25,13 +30,13 @@ class AmountVisualTransformation(
|
|||
val formattedText = if (formattedAmount.isNotEmpty() && symbol != null) {
|
||||
AnnotatedString(
|
||||
if (currencyCode != null) {
|
||||
BigDecimalFormatter.formatFiatEditableAmount(
|
||||
formatFiatEditableAmount(
|
||||
fiatAmount = formattedAmount,
|
||||
fiatCurrencyCode = currencyCode,
|
||||
fiatCurrencySymbol = symbol,
|
||||
)
|
||||
} else {
|
||||
BigDecimalFormatter.formatWithSymbol(formattedAmount, symbol)
|
||||
formatWithSymbol(formattedAmount, symbol)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
|
|
@ -45,6 +50,28 @@ class AmountVisualTransformation(
|
|||
)
|
||||
}
|
||||
|
||||
private fun formatFiatEditableAmount(
|
||||
fiatAmount: String?,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (fiatAmount == null) return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
|
||||
val numberFormatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
}
|
||||
val formatter = requireNotNull(numberFormatter as? DecimalFormat) {
|
||||
Timber.e("NumberFormat is null")
|
||||
return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
|
||||
}
|
||||
return "${formatter.positivePrefix}$fiatAmount${formatter.positiveSuffix}"
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
private fun formatWithSymbol(amount: String, symbol: String) = "$amount$CURRENCY_SPACE$symbol"
|
||||
|
||||
private class OffsetMappingImpl(
|
||||
private val text: String,
|
||||
private val formattedText: AnnotatedString,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import android.icu.text.CompactDecimalFormat
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
// == Formatters ==
|
||||
|
||||
/**
|
||||
* Formats the amount in compact format.
|
||||
* "123456.6" -> "$123.457K"
|
||||
* "12345.6" -> "$123.046K"
|
||||
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
|
||||
*/
|
||||
fun BigDecimalFiatFormat.compact(threeDigitsMethod: Boolean = false): BigDecimalFormat = BigDecimalFormat { value ->
|
||||
if (value < BigDecimal.ONE) {
|
||||
return@BigDecimalFormat defaultAmount()(value)
|
||||
}
|
||||
|
||||
val rawAmount = formatCompactAmount(
|
||||
amount = value,
|
||||
locale = locale,
|
||||
threeDigitsMethod = threeDigitsMethod,
|
||||
)
|
||||
|
||||
addFiatCurrencySymbolToStringAmount(
|
||||
amount = rawAmount,
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the amount in compact format.
|
||||
* "123456.6" -> "ETH 123.457K"
|
||||
* "12345.6" -> "123.046K ETH"
|
||||
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
|
||||
*/
|
||||
fun BigDecimalCryptoFormat.compact(threeDigitsMethod: Boolean = false): BigDecimalFormat = BigDecimalFormat { value ->
|
||||
if (value < BigDecimal.ONE) {
|
||||
return@BigDecimalFormat defaultAmount()(value)
|
||||
}
|
||||
|
||||
val rawAmount = formatCompactAmount(
|
||||
amount = value,
|
||||
locale = locale,
|
||||
threeDigitsMethod = threeDigitsMethod,
|
||||
)
|
||||
|
||||
addFiatCurrencySymbolToStringAmount(
|
||||
amount = rawAmount,
|
||||
fiatCurrencyCode = BigDecimalFormatConstants.usdCurrency.currencyCode,
|
||||
fiatCurrencySymbol = BigDecimalFormatConstants.usdCurrency.symbol,
|
||||
locale = locale,
|
||||
).replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = BigDecimalFormatConstants.usdCurrency.symbol,
|
||||
cryptoCurrencySymbol = symbol,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the amount in compact format.
|
||||
* ex. "123456.6" -> "123.46K", "12345.6" -> "123.05K"
|
||||
* Negative amount is not supported!
|
||||
*/
|
||||
fun BigDecimalFormatScope.rawCompact(locale: Locale = Locale.getDefault()) = BigDecimalFormat { value ->
|
||||
if (value < BigDecimal.ZERO) {
|
||||
return@BigDecimalFormat value.toPlainString()
|
||||
}
|
||||
|
||||
formatCompactAmount(
|
||||
amount = value,
|
||||
locale = locale,
|
||||
threeDigitsMethod = false,
|
||||
)
|
||||
}
|
||||
|
||||
// == Helpers ==
|
||||
|
||||
/**
|
||||
* "123456.6" -> "123.457K"
|
||||
* "12345.6" -> "123.046K"
|
||||
* 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
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
private fun formatCompactAmount(
|
||||
amount: BigDecimal,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
threeDigitsMethod: Boolean = false,
|
||||
): String {
|
||||
if (threeDigitsMethod) {
|
||||
val scaledAmount = amount.setScale(0, RoundingMode.HALF_UP)
|
||||
val digitsCount = scaledAmount.toString().count()
|
||||
val digitsToFormat = 6 - when (digitsCount % 3) {
|
||||
0 -> 0
|
||||
1 -> 2
|
||||
else -> 1
|
||||
}
|
||||
|
||||
val formatter = CompactDecimalFormat.getInstance(
|
||||
locale,
|
||||
CompactDecimalFormat.CompactStyle.SHORT,
|
||||
).apply {
|
||||
minimumSignificantDigits = 4
|
||||
maximumSignificantDigits = digitsToFormat
|
||||
}
|
||||
|
||||
return formatter.format(scaledAmount)
|
||||
} else {
|
||||
val scaledAmount = amount.setScale(0, RoundingMode.HALF_UP)
|
||||
val digitsCount = scaledAmount.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(scaledAmount)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a proper currency symbol for the provided formatted [amount]
|
||||
* ex. '10.0k" -> "$10.0k", "string" -> "$string"
|
||||
*/
|
||||
private fun addFiatCurrencySymbolToStringAmount(
|
||||
amount: String,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
val sampleAmount = BigDecimal.TEN
|
||||
val currency = getJavaCurrencyByCode(fiatCurrencyCode)
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
maximumFractionDigits = 0
|
||||
minimumFractionDigits = 0
|
||||
this.currency = currency
|
||||
}
|
||||
|
||||
val formatted = formatter.format(sampleAmount)
|
||||
.replace(currency.getSymbol(locale), fiatCurrencySymbol)
|
||||
.replace(sampleAmount.toString(), amount)
|
||||
|
||||
return formatted
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE_FORMAT_THRESHOLD
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.FORMAT_THRESHOLD
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.utils.extensions.isNotWhitespace
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
open class BigDecimalCryptoFormat(
|
||||
val symbol: String,
|
||||
val decimals: Int,
|
||||
val locale: Locale = Locale.getDefault(),
|
||||
) : BigDecimalFormat {
|
||||
|
||||
override fun invoke(value: BigDecimal): String = defaultAmount()(value)
|
||||
}
|
||||
|
||||
class BigDecimalCryptoFormatFull(
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
) : BigDecimalCryptoFormat(
|
||||
symbol = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
locale = locale,
|
||||
) {
|
||||
override fun invoke(value: BigDecimal): String = defaultAmount()(value)
|
||||
}
|
||||
|
||||
// == Initializers ==
|
||||
|
||||
fun BigDecimalFormatScope.crypto(
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): BigDecimalCryptoFormat {
|
||||
return BigDecimalCryptoFormat(
|
||||
symbol = symbol,
|
||||
decimals = decimals,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
fun BigDecimalFormatScope.crypto(
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): BigDecimalCryptoFormat {
|
||||
return BigDecimalCryptoFormat(
|
||||
symbol = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
// == Formatters ==
|
||||
|
||||
fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value ->
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
formatter.format(value)
|
||||
.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.getSymbol(locale),
|
||||
cryptoCurrencySymbol = symbol,
|
||||
)
|
||||
}
|
||||
|
||||
fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value ->
|
||||
val formatter = if (value.isMoreThanThreshold()) {
|
||||
NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = 2
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
} else {
|
||||
NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.DOWN
|
||||
}
|
||||
}
|
||||
|
||||
formatter.format(value)
|
||||
.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.getSymbol(locale),
|
||||
cryptoCurrencySymbol = symbol,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format for displaying crypto amounts with their original decimals.
|
||||
*/
|
||||
fun BigDecimalCryptoFormat.uncapped() = BigDecimalFormat { value ->
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = 2
|
||||
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".
|
||||
*/
|
||||
fun BigDecimalCryptoFormat.fee(canBeLower: Boolean = false) = BigDecimalFormat { value ->
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
if (value.lessThanFeeCryptoThreshold()) {
|
||||
buildString {
|
||||
append(CAN_BE_LOWER_SIGN)
|
||||
append(
|
||||
formatter
|
||||
.format(CRYPTO_FEE_FORMAT_THRESHOLD)
|
||||
.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.getSymbol(locale),
|
||||
cryptoCurrencySymbol = symbol,
|
||||
addStartSpace = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
buildString {
|
||||
if (canBeLower) {
|
||||
append(CAN_BE_LOWER_SIGN)
|
||||
}
|
||||
append(
|
||||
formatter.format(value)
|
||||
.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.getSymbol(locale),
|
||||
cryptoCurrencySymbol = symbol,
|
||||
addStartSpace = canBeLower,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// == Helpers ==
|
||||
|
||||
private fun BigDecimal.isMoreThanThreshold() = this > FORMAT_THRESHOLD
|
||||
|
||||
private fun BigDecimal.lessThanFeeCryptoThreshold() = this > BigDecimal.ZERO && this < CRYPTO_FEE_FORMAT_THRESHOLD
|
||||
|
||||
private val usdCurrency = Currency.getInstance(Locale.US)
|
||||
|
||||
// Replaces fiat currency symbol with crypto currency symbol
|
||||
// with respect to the position of the symbol and whitespace
|
||||
internal fun String.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol: String,
|
||||
cryptoCurrencySymbol: String,
|
||||
addStartSpace: Boolean = false,
|
||||
): 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(CURRENCY_SPACE)
|
||||
}
|
||||
|
||||
append(cryptoCurrencySymbol)
|
||||
}
|
||||
str.startsWith(fiatCurrencySymbol) -> {
|
||||
if (addStartSpace) {
|
||||
append(CURRENCY_SPACE)
|
||||
}
|
||||
|
||||
append(cryptoCurrencySymbol)
|
||||
|
||||
val withoutSymbol = str.drop(fiatCurrencySymbol.length)
|
||||
val first = withoutSymbol.firstOrNull()
|
||||
?: return cryptoCurrencySymbol
|
||||
|
||||
if (first.isNotWhitespace()) {
|
||||
append(CURRENCY_SPACE)
|
||||
}
|
||||
|
||||
append(withoutSymbol)
|
||||
}
|
||||
else -> append(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN
|
||||
import com.tangem.utils.StringsSigns.TILDE_SIGN
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
open class BigDecimalFiatFormat(
|
||||
val fiatCurrencyCode: String,
|
||||
val fiatCurrencySymbol: String,
|
||||
val locale: Locale = Locale.getDefault(),
|
||||
) : BigDecimalFormat {
|
||||
override fun invoke(p1: BigDecimal): String = error("")
|
||||
}
|
||||
|
||||
// == Initializers ==
|
||||
|
||||
fun BigDecimalFormatScope.fiat(
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): BigDecimalFiatFormat {
|
||||
return BigDecimalFiatFormat(
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
// == Formatters ==
|
||||
|
||||
/**
|
||||
* Formats fiat amount with default precision.
|
||||
*/
|
||||
fun BigDecimalFiatFormat.defaultAmount(): BigDecimalFormat = BigDecimalFormat { value ->
|
||||
val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
if (value.isLessThanThreshold()) {
|
||||
buildString {
|
||||
append(CAN_BE_LOWER_SIGN)
|
||||
append(
|
||||
formatter.format(FIAT_FORMAT_THRESHOLD)
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
formatter.format(value)
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats fiat amount with default precision and adds tilde sign
|
||||
*/
|
||||
fun BigDecimalFiatFormat.approximateAmount(): BigDecimalFormat = BigDecimalFormat { value ->
|
||||
val formattedAmount = defaultAmount()(value)
|
||||
|
||||
if (value.isLessThanThreshold()) {
|
||||
formattedAmount
|
||||
} else {
|
||||
buildString {
|
||||
append(TILDE_SIGN)
|
||||
append(formattedAmount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats fiat amount with extended precision.
|
||||
*/
|
||||
fun BigDecimalFiatFormat.uncapped(): BigDecimalFormat = BigDecimalFormat { value ->
|
||||
val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
|
||||
|
||||
val digits = if (value.isLessThanThreshold()) {
|
||||
FIAT_MARKET_EXTENDED_DIGITS
|
||||
} else {
|
||||
FIAT_MARKET_DEFAULT_DIGITS
|
||||
}
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
maximumFractionDigits = digits
|
||||
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
formatter.format(value)
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats fiat price with precision calculated based on the value.
|
||||
* @see getFiatPriceAmountWithScale
|
||||
*/
|
||||
fun BigDecimalFiatFormat.price(): BigDecimalFormat = BigDecimalFormat { value ->
|
||||
val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
|
||||
|
||||
val (priceAmount, finalScale) = getFiatPriceAmountWithScale(value = value)
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
maximumFractionDigits = finalScale
|
||||
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
formatter.format(priceAmount)
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
// == Helpers ==
|
||||
|
||||
private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD
|
||||
|
||||
private fun getFiatPriceAmountWithScale(value: BigDecimal): Pair<BigDecimal, Int> {
|
||||
return if (value < BigDecimal.ONE) {
|
||||
val leadingZeroes = value.scale() - value.precision()
|
||||
val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES
|
||||
|
||||
val amount = value
|
||||
.setScale(scale, RoundingMode.HALF_UP)
|
||||
.stripTrailingZeros()
|
||||
|
||||
amount to amount.scale()
|
||||
} else {
|
||||
value to FIAT_MARKET_DEFAULT_DIGITS
|
||||
}
|
||||
}
|
||||
|
||||
// == Constants ==
|
||||
|
||||
private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01")
|
||||
private const val FIAT_MARKET_DEFAULT_DIGITS = 2
|
||||
private const val FIAT_MARKET_EXTENDED_DIGITS = 6
|
||||
private const val FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES = 4
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface BigDecimalFormatScope {
|
||||
companion object { val Empty = object : BigDecimalFormatScope {} }
|
||||
}
|
||||
|
||||
fun interface BigDecimalFormat : (BigDecimal) -> String, BigDecimalFormatScope
|
||||
|
||||
inline fun BigDecimal.format(block: BigDecimalFormatScope.() -> BigDecimalFormat): String {
|
||||
return BigDecimalFormatScope.Empty.block()(this)
|
||||
}
|
||||
|
||||
inline fun BigDecimal?.format(
|
||||
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
|
||||
block: BigDecimalFormatScope.() -> BigDecimalFormat,
|
||||
): String {
|
||||
if (this == null) return fallbackString
|
||||
return BigDecimalFormatScope.Empty.block()(this)
|
||||
}
|
||||
|
||||
fun BigDecimal?.format(
|
||||
format: BigDecimalFormat,
|
||||
fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN,
|
||||
): String {
|
||||
if (this == null) return fallbackString
|
||||
return format(this)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import com.tangem.utils.StringsSigns.LOWER_SIGN
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
object BigDecimalFormatConstants {
|
||||
|
||||
const val EMPTY_BALANCE_SIGN = DASH_SIGN
|
||||
const val CAN_BE_LOWER_SIGN = LOWER_SIGN
|
||||
val FORMAT_THRESHOLD = BigDecimal("0.01")
|
||||
|
||||
const val CURRENCY_SPACE = '\u00a0'
|
||||
|
||||
val CRYPTO_FEE_FORMAT_THRESHOLD = BigDecimal("0.000001")
|
||||
|
||||
val usdCurrency: Currency by lazy { Currency.getInstance(Locale.US) }
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
class BigDecimalPercentFormat(
|
||||
val withoutSign: Boolean = true,
|
||||
val locale: Locale = Locale.getDefault(),
|
||||
) : BigDecimalFormat {
|
||||
override fun invoke(value: BigDecimal): String = default()(value)
|
||||
}
|
||||
|
||||
// == Initializers ==
|
||||
|
||||
fun BigDecimalFormatScope.percent(
|
||||
withoutSign: Boolean = true,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): BigDecimalPercentFormat {
|
||||
return BigDecimalPercentFormat(
|
||||
withoutSign = withoutSign,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
// == Formatters ==
|
||||
|
||||
private fun BigDecimalPercentFormat.default(): BigDecimalFormat = BigDecimalFormat { value ->
|
||||
val formatter = NumberFormat.getPercentInstance(locale).apply {
|
||||
maximumFractionDigits = 2
|
||||
minimumFractionDigits = 2
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
val valueToFormat = if (withoutSign) value.abs() else value
|
||||
|
||||
formatter.format(valueToFormat)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import java.util.Currency
|
||||
|
||||
fun getJavaCurrencyByCode(code: String): Currency {
|
||||
return runCatching { Currency.getInstance(code) }
|
||||
.getOrElse { e ->
|
||||
// Currency code is not valid ISO 4217 code
|
||||
if (e is IllegalArgumentException) {
|
||||
BigDecimalFormatConstants.usdCurrency
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,17 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import android.icu.text.CompactDecimalFormat
|
||||
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
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
object BigDecimalFormatter {
|
||||
|
||||
const val EMPTY_BALANCE_SIGN = DASH_SIGN
|
||||
|
|
@ -30,10 +27,7 @@ object BigDecimalFormatter {
|
|||
|
||||
private val usdCurrency = Currency.getInstance("USD")
|
||||
|
||||
@Deprecated(
|
||||
"Use formatCryptoAmount2",
|
||||
replaceWith = ReplaceWith("formatCryptoAmount2"),
|
||||
)
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatCryptoAmount(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: String,
|
||||
|
|
@ -58,30 +52,7 @@ 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,
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatCryptoAmountShorted(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: String,
|
||||
|
|
@ -115,6 +86,7 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatCryptoAmountUncapped(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
|
|
@ -138,6 +110,7 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatCryptoFeeAmount(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: String,
|
||||
|
|
@ -177,6 +150,7 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatCryptoAmount(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
|
|
@ -185,6 +159,7 @@ object BigDecimalFormatter {
|
|||
return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals, locale)
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatFiatAmount(
|
||||
fiatAmount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
|
|
@ -226,6 +201,7 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatFiatAmountUncapped(
|
||||
fiatAmount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
|
|
@ -251,6 +227,7 @@ object BigDecimalFormatter {
|
|||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatFiatPriceUncapped(
|
||||
fiatAmount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
|
|
@ -273,6 +250,7 @@ object BigDecimalFormatter {
|
|||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun getFiatPriceUncappedWithScale(value: BigDecimal): Pair<BigDecimal, Int> {
|
||||
return if (value < BigDecimal.ONE) {
|
||||
val leadingZeroes = value.scale() - value.precision()
|
||||
|
|
@ -288,26 +266,7 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
fun formatFiatEditableAmount(
|
||||
fiatAmount: String?,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (fiatAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatterCurrency = getCurrency(fiatCurrencyCode)
|
||||
val numberFormatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
}
|
||||
val formatter = requireNotNull(numberFormatter as? DecimalFormat) {
|
||||
Timber.e("NumberFormat is null")
|
||||
return EMPTY_BALANCE_SIGN
|
||||
}
|
||||
return "${formatter.positivePrefix}$fiatAmount${formatter.positiveSuffix}"
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
@Deprecated("Use BigDecimal.format")
|
||||
fun formatPercent(
|
||||
percent: BigDecimal,
|
||||
useAbsoluteValue: Boolean,
|
||||
|
|
@ -341,230 +300,6 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a proper currency sign for the provided formatted [amount]
|
||||
* ex. '10.0k" -> "$10.0k", "string" -> "$string"
|
||||
*/
|
||||
private fun addCurrencySymbolToStringAmount(
|
||||
amount: String,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
val sampleAmount = BigDecimal.TEN
|
||||
val currency = getCurrency(fiatCurrencyCode)
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
maximumFractionDigits = 0
|
||||
minimumFractionDigits = 0
|
||||
this.currency = currency
|
||||
}
|
||||
|
||||
val formatted = formatter.format(sampleAmount)
|
||||
.replace(currency.getSymbol(locale), fiatCurrencySymbol)
|
||||
.replace(sampleAmount.toString(), amount)
|
||||
|
||||
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"
|
||||
* 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
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun formatCompactFiatAmount(
|
||||
amount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
threeDigitsMethod: Boolean = false,
|
||||
scale: Int = 0,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (amount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
if (amount < BigDecimal.ONE) {
|
||||
return formatFiatPriceUncapped(
|
||||
fiatAmount = amount,
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
val rawAmount = formatCompactAmount(
|
||||
amount = amount,
|
||||
locale = locale,
|
||||
threeDigitsMethod = threeDigitsMethod,
|
||||
scale = scale,
|
||||
)
|
||||
|
||||
return addCurrencySymbolToStringAmount(
|
||||
amount = rawAmount,
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* "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"
|
||||
* 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
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun formatCompactAmount(
|
||||
amount: BigDecimal,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
threeDigitsMethod: Boolean = false,
|
||||
scale: Int = 0,
|
||||
): String {
|
||||
if (threeDigitsMethod) {
|
||||
val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP)
|
||||
val digitsCount = scaledAmount.toString().count()
|
||||
val digitsToFormat = 6 - when (digitsCount % 3) {
|
||||
0 -> 0
|
||||
1 -> 2
|
||||
else -> 1
|
||||
}
|
||||
|
||||
val formatter = CompactDecimalFormat.getInstance(
|
||||
locale,
|
||||
CompactDecimalFormat.CompactStyle.SHORT,
|
||||
).apply {
|
||||
minimumSignificantDigits = 4
|
||||
maximumSignificantDigits = digitsToFormat
|
||||
}
|
||||
|
||||
return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP))
|
||||
} else {
|
||||
val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP)
|
||||
val digitsCount = scaledAmount.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(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal.checkFiatThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD
|
||||
|
||||
private fun BigDecimal.checkCryptoThreshold() = this > BigDecimal.ZERO && this < CRYPTO_FEE_FORMAT_THRESHOLD
|
||||
|
|
|
|||
|
|
@ -0,0 +1,373 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
|
||||
internal class BigDecimalCryptoFormatTest {
|
||||
|
||||
private val testLocale = Locale.US
|
||||
private val testLocale2 = Locale.GERMANY
|
||||
private val symbol = "BTC"
|
||||
|
||||
// === defaultAmount() ===
|
||||
|
||||
@Test
|
||||
fun `defaultAmount (usually used as a user balance)`() {
|
||||
val testValue = BigDecimal("0.123456789999")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 8,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.12345679".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount (usually used as a user balance) alter locale`() {
|
||||
val testValue = BigDecimal("0.123456789999")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 8,
|
||||
locale = testLocale2,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0,12345679".addSymbolWithSpaceRight(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount decimals more than 8`() {
|
||||
val testValue = BigDecimal("0.123456789999")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.12345679".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount decimals more than 8 (short value)`() {
|
||||
val testValue = BigDecimal("0.12345")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.12345".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount decimals minimal (short value)`() {
|
||||
val testValue = BigDecimal("0.12345")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 2,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.12".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount less than 2 decimals`() {
|
||||
val testValue = BigDecimal("0.12345")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 0,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.12".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount grouping`() {
|
||||
val testValue = BigDecimal("12345678.11")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 0,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("12,345,678.11".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
// === shorted() ===
|
||||
|
||||
@Test
|
||||
fun `shorted amount smoke`() {
|
||||
val testValue = BigDecimal("50000.126123")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 8,
|
||||
locale = testLocale,
|
||||
).shorted()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("50,000.13".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shorted amount decimals less than 2 grouping`() {
|
||||
val testValue = BigDecimal("50000.126123")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 1,
|
||||
locale = testLocale,
|
||||
).shorted()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("50,000.13".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shorted amount less than threshold`() {
|
||||
val testValue = BigDecimal("0.0034567899")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 4,
|
||||
locale = testLocale,
|
||||
).shorted()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.0034".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shorted amount less than threshold, more decimals`() {
|
||||
val testValue = BigDecimal("0.00345678")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 8,
|
||||
locale = testLocale,
|
||||
).shorted()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.003456".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shorted amount diff locale half up`() {
|
||||
val testValue = BigDecimal("50000.126123")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 8,
|
||||
locale = testLocale2,
|
||||
).shorted()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("50.000,13".addSymbolWithSpaceRight(symbol))
|
||||
}
|
||||
|
||||
// === uncapped() ===
|
||||
|
||||
@Test
|
||||
fun `uncapped amount`() {
|
||||
val testValue = BigDecimal("50000.123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale,
|
||||
).uncapped()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("50,000.1234123412".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uncapped amount diff locale`() {
|
||||
val testValue = BigDecimal("50000.123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale2,
|
||||
).uncapped()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("50.000,1234123412".addSymbolWithSpaceRight(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uncapped amount half up`() {
|
||||
val testValue = BigDecimal("50000.12341234125")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale2,
|
||||
).uncapped()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("50.000,1234123413".addSymbolWithSpaceRight(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uncapped amount min decimals`() {
|
||||
val testValue = BigDecimal("50000.12341234125")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 1,
|
||||
locale = testLocale2,
|
||||
).uncapped()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("50.000,12".addSymbolWithSpaceRight(symbol))
|
||||
}
|
||||
|
||||
// === fee ===
|
||||
|
||||
@Test
|
||||
fun `fee amount`() {
|
||||
val testValue = BigDecimal("0.000123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale,
|
||||
).fee()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.000123".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fee amount diff locale`() {
|
||||
val testValue = BigDecimal("0.000123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale2,
|
||||
).fee()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0,000123".addSymbolWithSpaceRight(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fee amount canBeLower true`() {
|
||||
val testValue = BigDecimal("0.000123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale,
|
||||
).fee(canBeLower = true)
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("<" + CURRENCY_SPACE_FOR_TESTS + "0.000123".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fee amount canBeLower true (diff locale)`() {
|
||||
val testValue = BigDecimal("0.000123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale2,
|
||||
).fee(canBeLower = true)
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("<" + "0,000123".addSymbolWithSpaceRight(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fee amount lee than threshold`() {
|
||||
val testValue = BigDecimal("0.0000001234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 10,
|
||||
locale = testLocale,
|
||||
).fee()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("<" + CURRENCY_SPACE_FOR_TESTS + "0.000001".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fee amount min decimals half up`() {
|
||||
val testValue = BigDecimal("0.125412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
crypto(
|
||||
symbol = symbol,
|
||||
decimals = 1,
|
||||
locale = testLocale,
|
||||
).fee()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.13".addSymbolWithSpaceLeft(symbol))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
|
||||
internal class BigDecimalFiatFormatTest {
|
||||
|
||||
val testLocale = Locale.US
|
||||
val testLocale2 = Locale.GERMANY
|
||||
|
||||
val usdCurrencyCode = "USD"
|
||||
val usdSymbol = "$"
|
||||
|
||||
private fun String.addUsdSymbolLeft() = usdSymbol + this
|
||||
|
||||
// === defaultAmount() ===
|
||||
|
||||
@Test
|
||||
fun `defaultAmount smoke`() {
|
||||
val testValue = BigDecimal("1234.1234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("1,234.12".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount half up`() {
|
||||
val testValue = BigDecimal("1234.125")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("1,234.13".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount diff locale`() {
|
||||
val testValue = BigDecimal("1234.1234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale2,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("1.234,12".addSymbolWithSpaceRight(usdSymbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount less threshold`() {
|
||||
val testValue = BigDecimal("0.002234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("<" + "0.01".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultAmount less threshold diff locale`() {
|
||||
val testValue = BigDecimal("0.002234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale2,
|
||||
).defaultAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("<" + "0,01".addSymbolWithSpaceRight(usdSymbol))
|
||||
}
|
||||
|
||||
// === approximateAmount() ===
|
||||
|
||||
@Test
|
||||
fun `approximateAmount smoke`() {
|
||||
val testValue = BigDecimal("1234.1234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).approximateAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("~" + "1,234.12".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `approximateAmount half up`() {
|
||||
val testValue = BigDecimal("1234.125")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).approximateAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("~" + "1,234.13".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `approximateAmount diff locale`() {
|
||||
val testValue = BigDecimal("1234.1234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale2,
|
||||
).approximateAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("~" + "1.234,12".addSymbolWithSpaceRight(usdSymbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `approximateAmount less threshold`() {
|
||||
val testValue = BigDecimal("0.002234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).approximateAmount()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("<" + "0.01".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
// === uncapped() ===
|
||||
|
||||
@Test
|
||||
fun `uncapped smoke`() {
|
||||
val testValue = BigDecimal("1234.1234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).uncapped()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("1,234.12".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uncapped less threshold`() {
|
||||
val testValue = BigDecimal("0.00121")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).uncapped()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.00121".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uncapped decimals overflow`() {
|
||||
val testValue = BigDecimal("0.00123412341234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).uncapped()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.001234".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
// === price() ===
|
||||
|
||||
@Test
|
||||
fun `price smoke`() {
|
||||
val testValue = BigDecimal("1234.1234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).price()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("1,234.12".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `price diff locale`() {
|
||||
val testValue = BigDecimal("1234.1234")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale2,
|
||||
).price()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("1.234,12".addSymbolWithSpaceRight(usdSymbol))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `price less threshold`() {
|
||||
val testValue = BigDecimal("0.99987")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).price()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.9999".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `price less threshold more decimals strip zeros`() {
|
||||
val testValue = BigDecimal("0.0000123000")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).price()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.0000123".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `price less threshold too much decimals strip zeros`() {
|
||||
val testValue = BigDecimal("0.000000000000000000001230001234000")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).price()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.00000000000000000000123".addUsdSymbolLeft())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class BigDecimalFormatTest {
|
||||
|
||||
@Test
|
||||
fun smoke() {
|
||||
val value = BigDecimal("1234")
|
||||
val bgformat = BigDecimalFormat { bg ->
|
||||
bg.toPlainString() + "!"
|
||||
}
|
||||
val expected = "1234!"
|
||||
|
||||
Truth.assertThat(
|
||||
value.format(bgformat),
|
||||
).isEqualTo(expected)
|
||||
|
||||
Truth.assertThat(
|
||||
value.format { bgformat },
|
||||
).isEqualTo(expected)
|
||||
|
||||
Truth.assertThat(
|
||||
null.format(fallbackString = "!") { bgformat },
|
||||
).isEqualTo("!")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
|
||||
internal class BigDecimalPercentFormatTest {
|
||||
|
||||
val testLocale = Locale.US
|
||||
val testLocale2 = Locale.GERMANY
|
||||
|
||||
@Test
|
||||
fun smoke() {
|
||||
val value = BigDecimal("00.34")
|
||||
|
||||
val formatted = value.format {
|
||||
percent(locale = testLocale)
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted).isEqualTo("34.00%")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun negative() {
|
||||
val value = BigDecimal("00.34").negate()
|
||||
|
||||
val formatted = value.format {
|
||||
percent(locale = testLocale)
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted).isEqualTo("34.00%")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `negative with sign`() {
|
||||
val value = BigDecimal("00.34").negate()
|
||||
|
||||
val formatted = value.format {
|
||||
percent(
|
||||
withoutSign = false,
|
||||
locale = testLocale,
|
||||
)
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted).isEqualTo("-34.00%")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `default more decimals half up`() {
|
||||
val value = BigDecimal("00.345678").negate()
|
||||
|
||||
val formatted = value.format {
|
||||
percent(locale = testLocale)
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted).isEqualTo("34.57%")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `default diff locale`() {
|
||||
val value = BigDecimal("00.345678").negate()
|
||||
|
||||
val formatted = value.format {
|
||||
percent(locale = testLocale2)
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted).isEqualTo("34,57".addSymbolWithSpaceRight("%"))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.core.ui.format.bigdecimal
|
||||
|
||||
internal const val CURRENCY_SPACE_FOR_TESTS = '\u00a0'
|
||||
|
||||
internal fun String.addSymbolWithSpaceRight(symbol: String): String = "$this$CURRENCY_SPACE_FOR_TESTS$symbol"
|
||||
|
||||
internal fun String.addSymbolWithSpaceLeft(symbol: String): String = "$symbol$CURRENCY_SPACE_FOR_TESTS$this"
|
||||
|
|
@ -3,7 +3,10 @@ package com.tangem.features.markets.details.impl.model.converters
|
|||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.format.bigdecimal.compact
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.rawCompact
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
|
|
@ -142,14 +145,17 @@ internal class InsightsConverter(
|
|||
|
||||
private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String {
|
||||
val value = if (isFiatValue) {
|
||||
val currency = appCurrency()
|
||||
BigDecimalFormatter.formatCompactFiatAmount(
|
||||
amount = this.abs(),
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
)
|
||||
this.abs().format {
|
||||
val currency = appCurrency()
|
||||
fiat(
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
).compact()
|
||||
}
|
||||
} else {
|
||||
BigDecimalFormatter.formatCompactAmount(amount = this.abs())
|
||||
this.abs().format {
|
||||
rawCompact()
|
||||
}
|
||||
}
|
||||
|
||||
return when {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ package com.tangem.features.markets.details.impl.model.converters
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.format.bigdecimal.compact
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
|
||||
|
|
@ -129,18 +132,21 @@ internal class MetricsConverter(
|
|||
if (this == null) return StringsSigns.DASH_SIGN
|
||||
|
||||
return if (crypto) {
|
||||
BigDecimalFormatter.formatCompactCryptoAmount(
|
||||
amount = this,
|
||||
cryptoCurrencySymbol = tokenSymbol,
|
||||
)
|
||||
format {
|
||||
crypto(
|
||||
symbol = tokenSymbol,
|
||||
decimals = 2,
|
||||
).compact()
|
||||
}
|
||||
} else {
|
||||
val currency = appCurrency()
|
||||
|
||||
BigDecimalFormatter.formatCompactFiatAmount(
|
||||
amount = this,
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
)
|
||||
format {
|
||||
fiat(
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
).compact()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,9 @@ import com.tangem.common.ui.charts.state.MarketChartRawData
|
|||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.format.bigdecimal.compact
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
|
|
@ -71,12 +74,14 @@ internal class MarketsTokenItemConverter(
|
|||
private fun TokenMarket.getMarketCap(): String? {
|
||||
val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null
|
||||
|
||||
return BigDecimalFormatter.formatCompactFiatAmount(
|
||||
amount = value,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
threeDigitsMethod = true,
|
||||
)
|
||||
return value.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
).compact(
|
||||
threeDigitsMethod = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue