diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt index 89aaa014aa..54db33f525 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt @@ -143,7 +143,8 @@ fun BigDecimalCryptoFormatStyled.defaultAmount(spanStyleReference: SpanStyleRefe val formattedAmount = formatter.format(value) val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator - val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it).takeIf { i -> i >= 0 } } + ?: formattedAmount.length combinedReference( stringReference(formattedAmount.take(separatorIndex)), @@ -165,8 +166,9 @@ fun BigDecimalCryptoFormatStyled.defaultAmount(spanStyleReference: SpanStyleRefe cryptoCurrencySymbol = symbol, ) - val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator - val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.monetaryDecimalSeparator + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it).takeIf { i -> i >= 0 } } + ?: formattedAmount.length combinedReference( stringReference(formattedAmount.take(separatorIndex)), diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt index 3c78f403f3..643e492f71 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -97,7 +97,7 @@ fun BigDecimalFiatFormatStyled.defaultAmount(spanStyleReference: SpanStyleRefere value.zeroIfRoundsToZero(FIAT_MARKET_DEFAULT_DIGITS) } - val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.monetaryDecimalSeparator val currencySymbol = formatterCurrency.getSymbol(locale) val rawFormatted = formatter.format(formattingAmount) @@ -200,7 +200,7 @@ private fun BigDecimalFiatFormatStyled.price(spanStyleReference: SpanStyleRefere roundingMode = RoundingMode.HALF_UP } - val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.monetaryDecimalSeparator val currencySymbol = formatterCurrency.getSymbol(locale) val rawFormatted = formatter.format(priceAmount) diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt index fc4ef23fae..9a92f73d20 100644 --- a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt @@ -1,8 +1,13 @@ package com.tangem.core.ui.format.bigdecimal +import androidx.compose.ui.text.SpanStyle import com.google.common.truth.Truth +import com.tangem.core.ui.extensions.SpanStyleReference +import com.tangem.core.ui.extensions.TextReference import org.junit.jupiter.api.Test import java.math.BigDecimal +import java.text.DecimalFormat +import java.text.NumberFormat import java.util.Locale internal class BigDecimalCryptoFormatTest { @@ -10,6 +15,7 @@ internal class BigDecimalCryptoFormatTest { private val testLocale = Locale.US private val testLocale2 = Locale.GERMANY private val symbol = "BTC" + private val spanStyleStub = SpanStyleReference { SpanStyle() } // === defaultAmount() === @@ -125,6 +131,39 @@ internal class BigDecimalCryptoFormatTest { .isEqualTo("12,345,678.11".addSymbolWithSpaceLeft(symbol)) } + // === defaultAmount() styled === + + @Test + fun `GIVEN locale with distinct monetary separator WHEN styled defaultAmount THEN fraction split without crash`() { + // Arrange + // Regression: fr_CH plain separator is ',' but currency output uses '.' — indexOf(',') returned -1, + // and formattedAmount.take(-1) threw IllegalArgumentException + val swissLocale = Locale("fr", "CH") + val symbols = (NumberFormat.getCurrencyInstance(swissLocale) as DecimalFormat).decimalFormatSymbols + Truth.assertThat(symbols.monetaryDecimalSeparator).isNotEqualTo(symbols.decimalSeparator) + + val testValue = BigDecimal("12.34") + + // Act + val formatted = testValue.formatStyled { + cryptoStyled( + symbol = symbol, + decimals = 8, + spanStyleReference = spanStyleStub, + locale = swissLocale, + ) + } + + // Assert + val refs = (formatted as TextReference.Combined).refs.data + Truth.assertThat(refs).hasSize(2) + Truth.assertThat((refs[0] as TextReference.Str).value).isEqualTo("12") + + val fraction = refs[1] as TextReference.StyledStr + Truth.assertThat(fraction.value).startsWith("${symbols.monetaryDecimalSeparator}34") + Truth.assertThat(fraction.value).endsWith(symbol) + } + // === shorted() === @Test diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt index 5420e70b74..07f6b77573 100644 --- a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt @@ -1,11 +1,16 @@ package com.tangem.core.ui.format.bigdecimal +import androidx.compose.ui.text.SpanStyle import com.google.common.truth.Truth +import com.tangem.core.ui.extensions.SpanStyleReference +import com.tangem.core.ui.extensions.TextReference import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import java.math.BigDecimal +import java.text.DecimalFormat +import java.text.NumberFormat import java.util.Locale internal class BigDecimalFiatFormatTest { @@ -16,6 +21,8 @@ internal class BigDecimalFiatFormatTest { val usdCurrencyCode = "USD" val usdSymbol = "$" + private val spanStyleStub = SpanStyleReference { SpanStyle() } + private fun String.addUsdSymbolLeft() = usdSymbol + this // === defaultAmount() === @@ -132,6 +139,40 @@ internal class BigDecimalFiatFormatTest { .isEqualTo("-" + "0.01".addUsdSymbolLeft()) } + // === defaultAmount() styled === + + @Test + fun `GIVEN locale with distinct monetary separator WHEN styled defaultAmount THEN fraction split at monetary separator`() { + // Arrange + // fr_CH plain separator is ',' but currency output uses '.' — searching for the plain one + // failed to split the amount into whole and styled fractional parts + val swissLocale = Locale("fr", "CH") + val symbols = (NumberFormat.getCurrencyInstance(swissLocale) as DecimalFormat).decimalFormatSymbols + Truth.assertThat(symbols.monetaryDecimalSeparator).isNotEqualTo(symbols.decimalSeparator) + + val testValue = BigDecimal("12.34") + + // Act + val formatted = testValue.formatStyled { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + spanStyleReference = spanStyleStub, + locale = swissLocale, + ) + } + + // Assert + val refs = (formatted as TextReference.Combined).refs.data + Truth.assertThat(refs).hasSize(3) + Truth.assertThat(refs[0]).isEqualTo(TextReference.EMPTY) + Truth.assertThat((refs[1] as TextReference.Str).value).isEqualTo("12") + + val fraction = refs[2] as TextReference.StyledStr + Truth.assertThat(fraction.value).startsWith("${symbols.monetaryDecimalSeparator}34") + Truth.assertThat(fraction.value).endsWith(usdSymbol) + } + // === approximateAmount() === @Test diff --git a/core/ui/token-gen/README.md b/core/ui/token-gen/README.md index e170af92ef..e86fe2fd31 100644 --- a/core/ui/token-gen/README.md +++ b/core/ui/token-gen/README.md @@ -2,17 +2,19 @@ Generates Kotlin (Jetpack Compose) source files from design tokens and icons defined in the `ds-tokens` git submodule. +## Making sure submodule is at the pinned commit + +***For the most cases*** (a fresh checkout, or making sure the submodule is at the pinned commit), use: +```bash +git submodule update --init --recursive +``` + ## Updating tokens > **Note:** You only need `git submodule update --remote` when you want to pull **new** design tokens > from the remote `ds-tokens` repository. If you're just regenerating Kotlin from the tokens already > checked out (e.g. changing the generation script), **skip step 1** — don't run it without the need, > as it moves the submodule pointer to the latest remote commit and pulls in unrelated token changes. -> -> For all other cases (a fresh checkout, or making sure the submodule is at the pinned commit), use: -> ```bash -> git submodule update --init --recursive -> ``` > This checks out the submodule at the commit already recorded in the repo, without pulling anything new. 1. *(Only if you need newer tokens)* Update the `ds-tokens` submodule to the latest commit: