/* * This file is part of GNU Taler * (C) 2020 Taler Systems S.A. * * GNU Taler is free software; you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation; either version 3, or (at your option) any later version. * * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * GNU Taler; see the file COPYING. If not, see */ package net.taler.common import kotlinx.serialization.Decoder import kotlinx.serialization.Encoder import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.Serializer import kotlin.math.floor import kotlin.math.pow import kotlin.math.roundToInt class AmountParserException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause) class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause) @Serializable(with = KotlinXAmountSerializer::class) data class Amount( /** * name of the currency using either a three-character ISO 4217 currency code, * or a regional currency identifier starting with a "*" followed by at most 10 characters. * ISO 4217 exponents in the name are not supported, * although the "fraction" is corresponds to an ISO 4217 exponent of 6. */ val currency: String, /** * The integer part may be at most 2^52. * Note that "1" here would correspond to 1 EUR or 1 USD, depending on currency, not 1 cent. */ val value: Long, /** * Unsigned 32 bit fractional value to be added to value representing * an additional currency fraction, in units of one hundred millionth (1e-8) * of the base currency value. For example, a fraction * of 50_000_000 would correspond to 50 cents. */ val fraction: Int ) : Comparable { companion object { private const val FRACTIONAL_BASE: Int = 100000000 // 1e8 private val REGEX_CURRENCY = Regex("""^[-_*A-Za-z0-9]{1,12}$""") val MAX_VALUE = 2.0.pow(52).toLong() private const val MAX_FRACTION_LENGTH = 8 const val MAX_FRACTION = 99_999_999 fun zero(currency: String): Amount { return Amount(checkCurrency(currency), 0, 0) } fun fromJSONString(str: String): Amount { val split = str.split(":") if (split.size != 2) throw AmountParserException("Invalid Amount Format") return fromString(split[0], split[1]) } fun fromString(currency: String, str: String): Amount { // value val valueSplit = str.split(".") val value = checkValue(valueSplit[0].toLongOrNull()) // fraction val fraction: Int = if (valueSplit.size > 1) { 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) } else 0 return Amount(checkCurrency(currency), value, fraction) } fun min(currency: String): Amount = Amount(currency, 0, 1) fun max(currency: String): Amount = Amount(currency, MAX_VALUE, MAX_FRACTION) internal fun checkCurrency(currency: String): String { if (!REGEX_CURRENCY.matches(currency)) throw AmountParserException("Invalid currency: $currency") return currency } internal fun checkValue(value: Long?): Long { if (value == null || value > MAX_VALUE) throw AmountParserException("Value $value greater than $MAX_VALUE") return value } internal fun checkFraction(fraction: Int?): Int { if (fraction == null || fraction > MAX_FRACTION) throw AmountParserException("Fraction $fraction greater than $MAX_FRACTION") return fraction } } val amountStr: String get() = if (fraction == 0) "$value" else { var f = fraction var fractionStr = "" while (f > 0) { fractionStr += f / (FRACTIONAL_BASE / 10) f = (f * 10) % FRACTIONAL_BASE } "$value.$fractionStr" } 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() if (resultValue > MAX_VALUE) throw AmountOverflowException() val resultFraction = (fraction + other.fraction) % FRACTIONAL_BASE return Amount(currency, resultValue, resultFraction) } operator fun times(factor: Int): Amount { // TODO consider replacing with a faster implementation if (factor == 0) return zero(currency) var result = this for (i in 1 until factor) result += this return result } operator fun minus(other: Amount): Amount { check(currency == other.currency) { "Can only subtract from same currency" } var resultValue = value var resultFraction = fraction if (resultFraction < other.fraction) { if (resultValue < 1L) throw AmountOverflowException() resultValue-- resultFraction += FRACTIONAL_BASE } check(resultFraction >= other.fraction) resultFraction -= other.fraction if (resultValue < other.value) throw AmountOverflowException() resultValue -= other.value return Amount(currency, resultValue, resultFraction) } fun isZero(): Boolean { return value == 0L && fraction == 0 } fun toJSONString(): String { return "$currency:$amountStr" } override fun toString(): String { return "$amountStr $currency" } override fun compareTo(other: Amount): Int { check(currency == other.currency) { "Can only compare amounts with the same currency" } when { value == other.value -> { if (fraction < other.fraction) return -1 if (fraction > other.fraction) return 1 return 0 } value < other.value -> return -1 else -> return 1 } } } @Serializer(forClass = Amount::class) object KotlinXAmountSerializer: KSerializer { override fun serialize(encoder: Encoder, value: Amount) { encoder.encodeString(value.toJSONString()) } override fun deserialize(decoder: Decoder): Amount { return Amount.fromJSONString(decoder.decodeString()) } }