Skip to content

Commit

Permalink
[#1579] Zec/Fiat conversion APIs accept Locale
Browse files Browse the repository at this point in the history
* [#1579] Fix Fiat/Zec locale initial commit

* Changelog update

Closes #1579

* Bump library version for snapshot usage
  • Loading branch information
HonzaR authored Sep 6, 2024
1 parent fe223b9 commit 6b2bb61
Show file tree
Hide file tree
Showing 14 changed files with 113 additions and 135 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- Several functions have been updated to accept `cash.z.ecc.android.sdk.model.Locale` instead of
`cash.z.ecc.android.sdk.model.MonetarySeparators` as an argument. MonetarySeparators are derived from Locale now.
- `FiatCurrencyConversion.toZatoshi`
- `Zatoshi.toFiatCurrencyState`
- `Zatoshi.toFiatString`
- `BigDecimal.convertFiatDecimalToFiatString`
- `Zatoshi.Companion.fromZecString`

### Added
- `Double?.convertUsdToZec` has been added as we are moving away from `BigDecimal` in favor of primitive types
- `Locale.getDefault()` has been added

## [2.2.2] - 2024-09-03

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,6 @@ private fun SendMainContent(
recipientAddressString,
amountZecString,
memoString,
monetarySeparators
)

when (zecSendValidation) {
Expand Down Expand Up @@ -277,7 +276,6 @@ private fun SendMainContent(
recipientAddressString,
amountZecString,
memoString,
monetarySeparators
)

when (zecSendValidation) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import androidx.compose.ui.text.intl.Locale
import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.demoapp.ui.screen.transactions.view.TransactionState
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.TransactionOverview
import cash.z.ecc.android.sdk.model.toFiatCurrencyState
import kotlinx.datetime.Instant
Expand Down Expand Up @@ -33,7 +32,6 @@ internal fun TransactionOverview.toTransactionState(
feePaid?.toFiatCurrencyState(
currencyConversion = null,
locale = Locale.current.toKotlinLocale(),
monetarySeparators = MonetarySeparators.current(java.util.Locale.getDefault())
)?.toFiatCurrencyRateValue(context).orEmpty(),
status = transactionState.name,
onClick = onClick
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ ZCASH_ASCII_GPG_KEY=
# Configures whether release is an unstable snapshot, therefore published to the snapshot repository.
IS_SNAPSHOT=true

LIBRARY_VERSION=2.2.2
LIBRARY_VERSION=2.2.3

# Kotlin compiler warnings can be considered errors, failing the build.
ZCASH_IS_TREAT_WARNINGS_AS_ERRORS=true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cash.z.ecc.android.sdk.model
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.fixture.CurrencyConversionFixture
import cash.z.ecc.android.sdk.fixture.LocaleFixture
import cash.z.ecc.android.sdk.fixture.MonetarySeparatorsFixture
import cash.z.ecc.android.sdk.fixture.ZatoshiFixture
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
Expand All @@ -28,7 +27,6 @@ class FiatCurrencyConversionRateStateTest {
zatoshi.toFiatCurrencyState(
currencyConversion,
LocaleFixture.new(),
MonetarySeparatorsFixture.new(),
frozenClock
)

Expand All @@ -53,7 +51,6 @@ class FiatCurrencyConversionRateStateTest {
zatoshi.toFiatCurrencyState(
currencyConversion,
LocaleFixture.new(),
MonetarySeparatorsFixture.new(),
frozenClock
)

Expand All @@ -76,7 +73,6 @@ class FiatCurrencyConversionRateStateTest {
zatoshi.toFiatCurrencyState(
currencyConversion,
LocaleFixture.new(),
MonetarySeparatorsFixture.new(),
frozenClock
)

Expand All @@ -102,7 +98,6 @@ class FiatCurrencyConversionRateStateTest {
zatoshi.toFiatCurrencyState(
currencyConversion,
LocaleFixture.new(),
MonetarySeparatorsFixture.new(),
frozenClock
)

Expand All @@ -128,7 +123,6 @@ class FiatCurrencyConversionRateStateTest {
zatoshi.toFiatCurrencyState(
currencyConversion,
LocaleFixture.new(),
MonetarySeparatorsFixture.new(),
frozenClock
)

Expand All @@ -144,7 +138,6 @@ class FiatCurrencyConversionRateStateTest {
zatoshi.toFiatCurrencyState(
null,
LocaleFixture.new(),
MonetarySeparatorsFixture.new()
)

assertIs<FiatCurrencyConversionRateState.Unavailable>(result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import kotlin.test.assertTrue
class ZatoshiExtTest {
companion object {
private val EN_US_SEPARATORS = MonetarySeparatorsFixture.new()
private val EN_US_LOCALE = LocaleFixture.new("EN", "US")
private val CURRENCY_CONVERSION = CurrencyConversionFixture.new()
}

@Test
@SmallTest
fun zero_zatoshi_to_fiat_conversion_test() {
val zatoshi = ZatoshiFixture.new(0L)
val fiatString = zatoshi.toFiatString(CURRENCY_CONVERSION, LocaleFixture.new(), EN_US_SEPARATORS)
val fiatString = zatoshi.toFiatString(CURRENCY_CONVERSION, EN_US_LOCALE)

fiatString.also {
assertNotNull(it)
Expand All @@ -33,7 +34,7 @@ class ZatoshiExtTest {
@SmallTest
fun regular_zatoshi_to_fiat_conversion_test() {
val zatoshi = ZatoshiFixture.new(123_456_789L)
val fiatString = zatoshi.toFiatString(CURRENCY_CONVERSION, LocaleFixture.new(), EN_US_SEPARATORS)
val fiatString = zatoshi.toFiatString(CURRENCY_CONVERSION, EN_US_LOCALE)

fiatString.also {
assertNotNull(it)
Expand All @@ -54,15 +55,14 @@ class ZatoshiExtTest {
val fiatString =
roundedZatoshi.toFiatString(
roundedCurrencyConversion,
LocaleFixture.new(),
EN_US_SEPARATORS
EN_US_LOCALE
)

fiatString.also {
assertNotNull(it)
assertTrue(it.isNotEmpty())
assertTrue(it.isValidNumber(EN_US_SEPARATORS))
assertTrue("$100${EN_US_SEPARATORS.decimal}00" == it)
assertTrue("100${EN_US_SEPARATORS.decimal}00" == it)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import android.content.Context
import android.content.res.Configuration
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.fixture.MonetarySeparatorsFixture
import cash.z.ecc.android.sdk.fixture.LocaleFixture
import org.junit.Assert.assertEquals
import org.junit.Ignore
import org.junit.Test
Expand All @@ -14,7 +14,7 @@ import kotlin.test.assertNull

class ZecStringTest {
companion object {
private val EN_US_MONETARY_SEPARATORS = MonetarySeparatorsFixture.new()
private val EN_US_LOCALE = LocaleFixture.new("EN", "US")
private val context =
run {
val applicationContext = ApplicationProvider.getApplicationContext<Context>()
Expand All @@ -28,31 +28,31 @@ class ZecStringTest {

@Test
fun empty_string() {
val actual = Zatoshi.fromZecString(context, "", EN_US_MONETARY_SEPARATORS)
val actual = Zatoshi.fromZecString(context, "", EN_US_LOCALE)
val expected = null

assertEquals(expected, actual)
}

@Test
fun decimal_monetary_separator() {
val actual = Zatoshi.fromZecString(context, "1.13", EN_US_MONETARY_SEPARATORS)
val actual = Zatoshi.fromZecString(context, "1.13", EN_US_LOCALE)
val expected = Zatoshi(113000000L)

assertEquals(expected, actual)
}

@Test
fun comma_grouping_separator() {
val actual = Zatoshi.fromZecString(context, "1,130", EN_US_MONETARY_SEPARATORS)
val actual = Zatoshi.fromZecString(context, "1,130", EN_US_LOCALE)
val expected = Zatoshi(113000000000L)

assertEquals(expected, actual)
}

@Test
fun decimal_monetary_and() {
val actual = Zatoshi.fromZecString(context, "1,130", EN_US_MONETARY_SEPARATORS)
val actual = Zatoshi.fromZecString(context, "1,130", EN_US_LOCALE)
val expected = Zatoshi(113000000000L)

assertEquals(expected, actual)
Expand All @@ -71,30 +71,30 @@ class ZecStringTest {
@Ignore("https://github.com/zcash/zcash-android-wallet-sdk/issues/412")
fun round_trip() {
val expected = Zatoshi(113000000L)
val actual = Zatoshi.fromZecString(context, expected.toZecString(), EN_US_MONETARY_SEPARATORS)
val actual = Zatoshi.fromZecString(context, expected.toZecString(), EN_US_LOCALE)

assertEquals(expected, actual)
}

@Test
fun parse_bad_string() {
assertNull(Zatoshi.fromZecString(context, "", EN_US_MONETARY_SEPARATORS))
assertNull(Zatoshi.fromZecString(context, "+@#$~^&*=", EN_US_MONETARY_SEPARATORS))
assertNull(Zatoshi.fromZecString(context, "asdf", EN_US_MONETARY_SEPARATORS))
assertNull(Zatoshi.fromZecString(context, "", EN_US_LOCALE))
assertNull(Zatoshi.fromZecString(context, "+@#$~^&*=", EN_US_LOCALE))
assertNull(Zatoshi.fromZecString(context, "asdf", EN_US_LOCALE))
}

@Test
fun parse_invalid_numbers() {
assertNull(Zatoshi.fromZecString(context, "", EN_US_MONETARY_SEPARATORS))
assertNull(Zatoshi.fromZecString(context, "1,2", EN_US_MONETARY_SEPARATORS))
assertNull(Zatoshi.fromZecString(context, "1,23,", EN_US_MONETARY_SEPARATORS))
assertNull(Zatoshi.fromZecString(context, "1,234,", EN_US_MONETARY_SEPARATORS))
assertNull(Zatoshi.fromZecString(context, "", EN_US_LOCALE))
assertNull(Zatoshi.fromZecString(context, "1,2", EN_US_LOCALE))
assertNull(Zatoshi.fromZecString(context, "1,23,", EN_US_LOCALE))
assertNull(Zatoshi.fromZecString(context, "1,234,", EN_US_LOCALE))
}

@Test
@SmallTest
fun overflow_number_test() {
assertNotNull(Zatoshi.fromZecString(context, "21,000,000", EN_US_MONETARY_SEPARATORS))
assertNull(Zatoshi.fromZecString(context, "21,000,001", EN_US_MONETARY_SEPARATORS))
assertNotNull(Zatoshi.fromZecString(context, "21,000,000", EN_US_LOCALE))
assertNull(Zatoshi.fromZecString(context, "21,000,001", EN_US_LOCALE))
}
}
Original file line number Diff line number Diff line change
@@ -1,58 +1,45 @@
package cash.z.ecc.android.sdk.model

import android.content.Context
import android.icu.math.BigDecimal
import android.icu.text.DecimalFormat
import android.icu.text.NumberFormat
import cash.z.ecc.android.sdk.ext.convertUsdToZec
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.text.ParseException
import java.util.Locale

fun FiatCurrencyConversion.toZatoshi(
context: Context,
value: String,
monetarySeparators: MonetarySeparators = MonetarySeparators.current(Locale.getDefault()),
locale: Locale = Locale.getDefault(),
): Zatoshi? {
if (!ZecStringExt.filterConfirm(context, monetarySeparators, value)) {
if (!ZecStringExt.filterConfirm(
context = context,
separators = MonetarySeparators.current(locale.toJavaLocale()),
zecString = value
)
) {
return null
}

val symbols =
DecimalFormatSymbols.getInstance(Locale.US).apply {
this.decimalSeparator = monetarySeparators.decimal
if (monetarySeparators.isGroupingValid()) {
this.groupingSeparator = monetarySeparators.grouping
}
}

val localizedPattern =
if (monetarySeparators.isGroupingValid()) {
"#${monetarySeparators.grouping}##0${monetarySeparators.decimal}0#"
} else {
"0${monetarySeparators.decimal}0#"
}

val decimalFormat =
DecimalFormat(localizedPattern, symbols).apply {
isParseBigDecimal = true
roundingMode = RoundingMode.HALF_EVEN // aka Bankers rounding
DecimalFormat.getInstance(locale.toJavaLocale(), NumberFormat.NUMBERSTYLE).apply {
// TODO [#343]: https://github.com/zcash/secant-android-wallet/issues/343
roundingMode = BigDecimal.ROUND_HALF_EVEN // aka Bankers rounding
maximumFractionDigits = FRACTION_DIGITS
minimumFractionDigits = FRACTION_DIGITS
}

// TODO [#343]: https://github.com/zcash/secant-android-wallet/issues/343
val bigDecimal =
val doubleValue =
try {
decimalFormat.parse(value) as BigDecimal
} catch (e: NumberFormatException) {
null
decimalFormat.parse(value).toDouble()
} catch (e: ParseException) {
null
}

@Suppress("SwallowedException")
return try {
bigDecimal.convertUsdToZec(priceOfZec.toBigDecimal()).toDouble()
doubleValue.convertUsdToZec(priceOfZec)
.convertZecToZatoshi()
} catch (e: IllegalArgumentException) {
null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cash.z.ecc.android.sdk.model

data class Locale(val language: String, val region: String?, val variant: String?) {
companion object
companion object {
fun getDefault() = java.util.Locale.getDefault().toKotlinLocale()
}
}

fun Locale.toJavaLocale(): java.util.Locale {
Expand Down
Loading

0 comments on commit 6b2bb61

Please sign in to comment.