diff options
Diffstat (limited to 'taler-kotlin-android/src/main/java/net/taler/common/Amount.kt')
-rw-r--r-- | taler-kotlin-android/src/main/java/net/taler/common/Amount.kt | 104 |
1 files changed, 88 insertions, 16 deletions
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt index 4a6e4b3..3e3bd0a 100644 --- a/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt +++ b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt @@ -16,17 +16,24 @@ package net.taler.common +import android.os.Build import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.Serializer import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.text.NumberFormat import kotlin.math.floor import kotlin.math.pow import kotlin.math.roundToInt -public class AmountParserException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause) -public class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause) +public class AmountParserException(msg: String? = null, cause: Throwable? = null) : + Exception(msg, cause) + +public class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : + Exception(msg, cause) @Serializable(with = KotlinXAmountSerializer::class) public data class Amount( @@ -50,7 +57,12 @@ public data class Amount( * of the base currency value. For example, a fraction * of 50_000_000 would correspond to 50 cents. */ - val fraction: Int + val fraction: Int, + + /** + * Currency specification for amount + */ + val spec: CurrencySpecification? = null, ) : Comparable<Amount> { public companion object { @@ -62,12 +74,6 @@ public data class Amount( private const val MAX_FRACTION_LENGTH = 8 public const val MAX_FRACTION: Int = 99_999_999 - public fun fromDouble(currency: String, value: Double): Amount { - val intPart = Math.floor(value).toLong() - val fraPart = Math.floor((value - intPart) * FRACTIONAL_BASE).toInt() - return Amount(currency, intPart, fraPart) - } - public fun zero(currency: String): Amount { return Amount(checkCurrency(currency), 0, 0) } @@ -87,14 +93,35 @@ public data class Amount( val fractionStr = valueSplit[1] if (fractionStr.length > MAX_FRACTION_LENGTH) throw AmountParserException("Fraction $fractionStr too long") - val fraction = "0.$fractionStr".toDoubleOrNull() - ?.times(FRACTIONAL_BASE) - ?.roundToInt() - checkFraction(fraction) + checkFraction(fractionStr.getFraction()) } else 0 return Amount(checkCurrency(currency), value, fraction) } + public fun isValidAmountStr(str: String): Boolean { + if (str.count { it == '.' } > 1) return false + val split = str.split(".") + try { + checkValue(split[0].toLongOrNull()) + } catch (e: AmountParserException) { + return false + } + // also check fraction, if it exists + if (split.size > 1) { + val fractionStr = split[1] + if (fractionStr.length > MAX_FRACTION_LENGTH) return false + val fraction = fractionStr.getFraction() ?: return false + return fraction <= MAX_FRACTION + } + return true + } + + private fun String.getFraction(): Int? { + return "0.$this".toDoubleOrNull() + ?.times(FRACTIONAL_BASE) + ?.roundToInt() + } + public fun min(currency: String): Amount = Amount(currency, 0, 1) public fun max(currency: String): Amount = Amount(currency, MAX_VALUE, MAX_FRACTION) @@ -132,7 +159,8 @@ public data class Amount( public operator fun plus(other: Amount): Amount { check(currency == other.currency) { "Can only subtract from same currency" } - val resultValue = value + other.value + floor((fraction + other.fraction).toDouble() / FRACTIONAL_BASE).toLong() + val resultValue = + value + other.value + floor((fraction + other.fraction).toDouble() / FRACTIONAL_BASE).toLong() if (resultValue > MAX_VALUE) throw AmountOverflowException() val resultFraction = (fraction + other.fraction) % FRACTIONAL_BASE @@ -151,6 +179,8 @@ public data class Amount( return Amount(checkCurrency(currency), this.value, this.fraction) } + fun withSpec(spec: CurrencySpecification?) = copy(spec = spec) + public operator fun minus(other: Amount): Amount { check(currency == other.currency) { "Can only subtract from same currency" } var resultValue = value @@ -177,8 +207,50 @@ public data class Amount( return "$currency:$amountStr" } - override fun toString(): String { - return "$amountStr $currency" + override fun toString() = toString( + showSymbol = true, + negative = false, + ) + + fun toString( + showSymbol: Boolean = true, + negative: Boolean = false, + symbols: DecimalFormatSymbols = DecimalFormat().decimalFormatSymbols, + ): String { + // We clone the object to safely/cleanly modify it + val s = symbols.clone() as DecimalFormatSymbols + val amount = (if (negative) "-$amountStr" else amountStr).toBigDecimal() + + // No currency spec, so we render normally + if (spec == null) { + val format = NumberFormat.getInstance() + format.maximumFractionDigits = MAX_FRACTION_LENGTH + format.minimumFractionDigits = 0 + if (Build.VERSION.SDK_INT >= 34) { + s.groupingSeparator = s.monetaryGroupingSeparator + } + s.decimalSeparator = s.monetaryDecimalSeparator + (format as DecimalFormat).decimalFormatSymbols = s + + val fmt = format.format(amount) + return if (showSymbol) "$fmt $currency" else fmt + } + + // There is currency spec, so we can do things right + val format = NumberFormat.getCurrencyInstance() + format.maximumFractionDigits = spec.numFractionalNormalDigits + format.minimumFractionDigits = spec.numFractionalTrailingZeroDigits + s.currencySymbol = spec.symbol ?: "" + (format as DecimalFormat).decimalFormatSymbols = s + + val fmt = format.format(amount) + return if (showSymbol) { + // If no symbol, then we use the currency string + if (spec.symbol != null) fmt else "$fmt $currency" + } else { + // We should do better than manually removing the symbol here + fmt.replace(s.currencySymbol, "").trim() + } } override fun compareTo(other: Amount): Int { |