aboutsummaryrefslogtreecommitdiff
path: root/taler-kotlin-common
diff options
context:
space:
mode:
Diffstat (limited to 'taler-kotlin-common')
-rw-r--r--taler-kotlin-common/build.gradle5
-rw-r--r--taler-kotlin-common/src/main/java/net/taler/common/Amount.kt192
-rw-r--r--taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt7
-rw-r--r--taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt19
-rw-r--r--taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt40
-rw-r--r--taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt290
6 files changed, 520 insertions, 33 deletions
diff --git a/taler-kotlin-common/build.gradle b/taler-kotlin-common/build.gradle
index 1d45a54..1c53839 100644
--- a/taler-kotlin-common/build.gradle
+++ b/taler-kotlin-common/build.gradle
@@ -60,4 +60,9 @@ dependencies {
// JSON parsing and serialization
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.2"
+
+ lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha'
+
+ testImplementation 'junit:junit:4.13'
+ testImplementation 'org.json:json:20190722'
}
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt b/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt
index 0389db1..48bd643 100644
--- a/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt
+++ b/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt
@@ -16,47 +16,185 @@
package net.taler.common
+import android.annotation.SuppressLint
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.JsonMappingException
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import org.json.JSONObject
+import java.lang.Math.floorDiv
+import kotlin.math.pow
+import kotlin.math.roundToInt
-data class Amount(val currency: String, val amount: String) {
+class AmountDeserializer : StdDeserializer<Amount>(Amount::class.java) {
+ override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Amount {
+ val node = p.codec.readValue(p, String::class.java)
+ try {
+ return Amount.fromJSONString(node)
+ } catch (e: AmountParserException) {
+ throw JsonMappingException(p, "Error parsing Amount", e)
+ }
+ }
+}
+
+class AmountParserException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause)
+class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause)
+
+@JsonDeserialize(using = AmountDeserializer::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
+) {
companion object {
- private const val FRACTIONAL_BASE = 1e8
- private val SIGNED_REGEX = Regex("""([+\-])(\w+):([0-9.]+)""")
- fun fromString(strAmount: String): Amount {
- val components = strAmount.split(":")
- return Amount(components[0], components[1])
+ private const val FRACTIONAL_BASE: Int = 100000000 // 1e8
+
+ @Suppress("unused")
+ private val REGEX = Regex("""^[-_*A-Za-z0-9]{1,12}:([0-9]+)\.?([0-9]+)?$""")
+ private val REGEX_CURRENCY = Regex("""^[-_*A-Za-z0-9]{1,12}$""")
+ private val MAX_VALUE = 2.0.pow(52)
+ private const val MAX_FRACTION_LENGTH = 8
+ private const val MAX_FRACTION = 99_999_999
+
+ @Throws(AmountParserException::class)
+ @SuppressLint("CheckedExceptions")
+ fun zero(currency: String): Amount {
+ return Amount(checkCurrency(currency), 0, 0)
+ }
+
+ @Throws(AmountParserException::class)
+ @SuppressLint("CheckedExceptions")
+ fun fromJSONString(str: String): Amount {
+ val split = str.split(":")
+ if (split.size != 2) throw AmountParserException("Invalid Amount Format")
+ // currency
+ val currency = checkCurrency(split[0])
+ // value
+ val valueSplit = split[1].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(currency, value, fraction)
+ }
+
+ @Throws(AmountParserException::class)
+ @SuppressLint("CheckedExceptions")
+ fun fromJsonObject(json: JSONObject): Amount {
+ val currency = checkCurrency(json.optString("currency"))
+ val value = checkValue(json.optString("value").toLongOrNull())
+ val fraction = checkFraction(json.optString("fraction").toIntOrNull())
+ return Amount(currency, value, fraction)
+ }
+
+ @Throws(AmountParserException::class)
+ private fun checkCurrency(currency: String): String {
+ if (!REGEX_CURRENCY.matches(currency))
+ throw AmountParserException("Invalid currency: $currency")
+ return currency
+ }
+
+ @Throws(AmountParserException::class)
+ private fun checkValue(value: Long?): Long {
+ if (value == null || value > MAX_VALUE)
+ throw AmountParserException("Value $value greater than $MAX_VALUE")
+ return value
}
- fun fromStringSigned(strAmount: String): Amount? {
- val groups = SIGNED_REGEX.matchEntire(strAmount)?.groupValues ?: emptyList()
- if (groups.size < 4) return null
- var amount = groups[3].toDoubleOrNull() ?: return null
- if (groups[1] == "-") amount *= -1
- val currency = groups[2]
- val amountStr = amount.toString()
- // only display as many digits as required to precisely render the balance
- return Amount(currency, amountStr.removeSuffix(".0"))
+ @Throws(AmountParserException::class)
+ private 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"
}
- fun fromJson(jsonAmount: JSONObject): Amount {
- val amountCurrency = jsonAmount.getString("currency")
- val amountValue = jsonAmount.getString("value")
- val amountFraction = jsonAmount.getString("fraction")
- val amountIntValue = Integer.parseInt(amountValue)
- val amountIntFraction = Integer.parseInt(amountFraction)
- return Amount(
- amountCurrency,
- (amountIntValue + amountIntFraction / FRACTIONAL_BASE).toString()
- )
+ @Throws(AmountOverflowException::class)
+ operator fun plus(other: Amount): Amount {
+ check(currency == other.currency) { "Can only subtract from same currency" }
+ val resultValue = value + other.value + floorDiv(fraction + other.fraction, FRACTIONAL_BASE)
+ if (resultValue > MAX_VALUE)
+ throw AmountOverflowException()
+ val resultFraction = (fraction + other.fraction) % FRACTIONAL_BASE
+ return Amount(currency, resultValue, resultFraction)
+ }
+
+ @Throws(AmountOverflowException::class)
+ operator fun times(factor: Int): Amount {
+ var result = this
+ for (i in 1 until factor) result += this
+ return result
+ }
+
+ @Throws(AmountOverflowException::class)
+ 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 amount.toDouble() == 0.0
+ return value == 0L && fraction == 0
}
- override fun toString() = "$amount $currency"
+ fun toJSONString(): String {
+ return "$currency:$amountStr"
+ }
+
+ override fun toString(): String {
+ return "$amountStr $currency"
+ }
}
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt b/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt
index fc04da5..5bc5721 100644
--- a/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt
+++ b/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt
@@ -21,7 +21,6 @@ import android.content.Context.CONNECTIVITY_SERVICE
import android.net.ConnectivityManager
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.os.Build.VERSION.SDK_INT
-import android.text.format.DateUtils
import android.text.format.DateUtils.DAY_IN_MILLIS
import android.text.format.DateUtils.FORMAT_ABBREV_MONTH
import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE
@@ -29,6 +28,8 @@ import android.text.format.DateUtils.FORMAT_NO_YEAR
import android.text.format.DateUtils.FORMAT_SHOW_DATE
import android.text.format.DateUtils.FORMAT_SHOW_TIME
import android.text.format.DateUtils.MINUTE_IN_MILLIS
+import android.text.format.DateUtils.formatDateTime
+import android.text.format.DateUtils.getRelativeTimeSpanString
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
@@ -78,6 +79,6 @@ fun Long.toRelativeTime(context: Context): CharSequence {
val now = System.currentTimeMillis()
return if (now - this > DAY_IN_MILLIS * 2) {
val flags = FORMAT_SHOW_TIME or FORMAT_SHOW_DATE or FORMAT_ABBREV_MONTH or FORMAT_NO_YEAR
- DateUtils.formatDateTime(context, this, flags)
- } else DateUtils.getRelativeTimeSpanString(this, now, MINUTE_IN_MILLIS, FORMAT_ABBREV_RELATIVE)
+ formatDateTime(context, this, flags)
+ } else getRelativeTimeSpanString(this, now, MINUTE_IN_MILLIS, FORMAT_ABBREV_RELATIVE)
}
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt b/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt
index 1e70b6f..cd417ef 100644
--- a/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt
+++ b/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt
@@ -18,12 +18,20 @@ package net.taler.common
import androidx.annotation.RequiresApi
import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY
import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL
import com.fasterxml.jackson.annotation.JsonProperty
import net.taler.common.TalerUtils.getLocalizedString
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class ContractTerms(
+ val summary: String,
+ val products: List<ContractProduct>,
+ val amount: Amount
+)
+
@JsonInclude(NON_NULL)
abstract class Product {
@get:JsonProperty("product_id")
@@ -32,7 +40,7 @@ abstract class Product {
@get:JsonProperty("description_i18n")
abstract val descriptionI18n: Map<String, String>?
- abstract val price: String
+ abstract val price: Amount
@get:JsonProperty("delivery_location")
abstract val location: String?
@@ -48,11 +56,16 @@ data class ContractProduct(
override val productId: String?,
override val description: String,
override val descriptionI18n: Map<String, String>?,
- override val price: String,
+ override val price: Amount,
override val location: String?,
override val image: String?,
val quantity: Int
-) : Product()
+) : Product() {
+ @get:JsonIgnore
+ val totalPrice: Amount by lazy {
+ price * quantity
+ }
+}
@JsonInclude(NON_EMPTY)
class Timestamp(
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt b/taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt
new file mode 100644
index 0000000..03a0d6e
--- /dev/null
+++ b/taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt
@@ -0,0 +1,40 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+import android.annotation.SuppressLint
+
+data class SignedAmount(
+ val positive: Boolean,
+ val amount: Amount
+) {
+
+ companion object {
+ @Throws(AmountParserException::class)
+ @SuppressLint("CheckedExceptions")
+ fun fromJSONString(str: String): SignedAmount = when (str.substring(0, 1)) {
+ "-" -> SignedAmount(false, Amount.fromJSONString(str.substring(1)))
+ "+" -> SignedAmount(true, Amount.fromJSONString(str.substring(1)))
+ else -> SignedAmount(true, Amount.fromJSONString(str))
+ }
+ }
+
+ override fun toString(): String {
+ return if (positive) "$amount" else "-$amount"
+ }
+
+} \ No newline at end of file
diff --git a/taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt b/taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt
new file mode 100644
index 0000000..c09da3c
--- /dev/null
+++ b/taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt
@@ -0,0 +1,290 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import com.fasterxml.jackson.module.kotlin.readValue
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+
+class AmountTest {
+
+ @Test
+ fun `test fromJSONString() works`() {
+ var str = "TESTKUDOS:23.42"
+ var amount = Amount.fromJSONString(str)
+ assertEquals(str, amount.toJSONString())
+ assertEquals("TESTKUDOS", amount.currency)
+ assertEquals(23, amount.value)
+ assertEquals((0.42 * 1e8).toInt(), amount.fraction)
+ assertEquals("23.42 TESTKUDOS", amount.toString())
+
+ str = "EUR:500000000.00000001"
+ amount = Amount.fromJSONString(str)
+ assertEquals(str, amount.toJSONString())
+ assertEquals("EUR", amount.currency)
+ assertEquals(500000000, amount.value)
+ assertEquals(1, amount.fraction)
+ assertEquals("500000000.00000001 EUR", amount.toString())
+
+ str = "EUR:1500000000.00000003"
+ amount = Amount.fromJSONString(str)
+ assertEquals(str, amount.toJSONString())
+ assertEquals("EUR", amount.currency)
+ assertEquals(1500000000, amount.value)
+ assertEquals(3, amount.fraction)
+ assertEquals("1500000000.00000003 EUR", amount.toString())
+ }
+
+ @Test
+ fun `test fromJSONString() accepts max values, rejects above`() {
+ val maxValue = 4503599627370496
+ val str = "TESTKUDOS123:$maxValue.99999999"
+ val amount = Amount.fromJSONString(str)
+ assertEquals(str, amount.toJSONString())
+ assertEquals("TESTKUDOS123", amount.currency)
+ assertEquals(maxValue, amount.value)
+ assertEquals("$maxValue.99999999 TESTKUDOS123", amount.toString())
+
+ // longer currency not accepted
+ assertThrows<AmountParserException>("longer currency was accepted") {
+ Amount.fromJSONString("TESTKUDOS1234:$maxValue.99999999")
+ }
+
+ // max value + 1 not accepted
+ assertThrows<AmountParserException>("max value + 1 was accepted") {
+ Amount.fromJSONString("TESTKUDOS123:${maxValue + 1}.99999999")
+ }
+
+ // max fraction + 1 not accepted
+ assertThrows<AmountParserException>("max fraction + 1 was accepted") {
+ Amount.fromJSONString("TESTKUDOS123:$maxValue.999999990")
+ }
+ }
+
+ @Test
+ fun `test JSON deserialization()`() {
+ val mapper = ObjectMapper().registerModule(KotlinModule())
+ var str = "TESTKUDOS:23.42"
+ var amount: Amount = mapper.readValue("\"$str\"")
+ assertEquals(str, amount.toJSONString())
+ assertEquals("TESTKUDOS", amount.currency)
+ assertEquals(23, amount.value)
+ assertEquals((0.42 * 1e8).toInt(), amount.fraction)
+ assertEquals("23.42 TESTKUDOS", amount.toString())
+
+ str = "EUR:500000000.00000001"
+ amount = mapper.readValue("\"$str\"")
+ assertEquals(str, amount.toJSONString())
+ assertEquals("EUR", amount.currency)
+ assertEquals(500000000, amount.value)
+ assertEquals(1, amount.fraction)
+ assertEquals("500000000.00000001 EUR", amount.toString())
+
+ str = "EUR:1500000000.00000003"
+ amount = mapper.readValue("\"$str\"")
+ assertEquals(str, amount.toJSONString())
+ assertEquals("EUR", amount.currency)
+ assertEquals(1500000000, amount.value)
+ assertEquals(3, amount.fraction)
+ assertEquals("1500000000.00000003 EUR", amount.toString())
+ }
+
+ @Test
+ fun `test fromJSONString() rejections`() {
+ assertThrows<AmountParserException> {
+ Amount.fromJSONString("TESTKUDOS:0,5")
+ }
+ assertThrows<AmountParserException> {
+ Amount.fromJSONString("+TESTKUDOS:0.5")
+ }
+ assertThrows<AmountParserException> {
+ Amount.fromJSONString("0.5")
+ }
+ assertThrows<AmountParserException> {
+ Amount.fromJSONString(":0.5")
+ }
+ assertThrows<AmountParserException> {
+ Amount.fromJSONString("EUR::0.5")
+ }
+ assertThrows<AmountParserException> {
+ Amount.fromJSONString("EUR:.5")
+ }
+ }
+
+ @Test
+ fun `test fromJsonObject() works`() {
+ val map = mapOf(
+ "currency" to "TESTKUDOS",
+ "value" to "23",
+ "fraction" to "42000000"
+ )
+
+ val amount = Amount.fromJsonObject(JSONObject(map))
+ assertEquals("TESTKUDOS:23.42", amount.toJSONString())
+ assertEquals("TESTKUDOS", amount.currency)
+ assertEquals(23, amount.value)
+ assertEquals(42000000, amount.fraction)
+ assertEquals("23.42 TESTKUDOS", amount.toString())
+ }
+
+ @Test
+ fun `test fromJsonObject() accepts max values, rejects above`() {
+ val maxValue = 4503599627370496
+ val maxFraction = 99999999
+ var map = mapOf(
+ "currency" to "TESTKUDOS123",
+ "value" to "$maxValue",
+ "fraction" to "$maxFraction"
+ )
+
+ val amount = Amount.fromJsonObject(JSONObject(map))
+ assertEquals("TESTKUDOS123:$maxValue.$maxFraction", amount.toJSONString())
+ assertEquals("TESTKUDOS123", amount.currency)
+ assertEquals(maxValue, amount.value)
+ assertEquals(maxFraction, amount.fraction)
+ assertEquals("$maxValue.$maxFraction TESTKUDOS123", amount.toString())
+
+ // longer currency not accepted
+ assertThrows<AmountParserException>("longer currency was accepted") {
+ map = mapOf(
+ "currency" to "TESTKUDOS1234",
+ "value" to "$maxValue",
+ "fraction" to "$maxFraction"
+ )
+ Amount.fromJsonObject(JSONObject(map))
+ }
+
+ // max value + 1 not accepted
+ assertThrows<AmountParserException>("max value + 1 was accepted") {
+ map = mapOf(
+ "currency" to "TESTKUDOS123",
+ "value" to "${maxValue + 1}",
+ "fraction" to "$maxFraction"
+ )
+ Amount.fromJsonObject(JSONObject(map))
+ }
+
+ // max fraction + 1 not accepted
+ assertThrows<AmountParserException>("max fraction + 1 was accepted") {
+ map = mapOf(
+ "currency" to "TESTKUDOS123",
+ "value" to "$maxValue",
+ "fraction" to "${maxFraction + 1}"
+ )
+ Amount.fromJsonObject(JSONObject(map))
+ }
+ }
+
+ @Test
+ fun `test addition`() {
+ assertEquals(
+ Amount.fromJSONString("EUR:2"),
+ Amount.fromJSONString("EUR:1") + Amount.fromJSONString("EUR:1")
+ )
+ assertEquals(
+ Amount.fromJSONString("EUR:3"),
+ Amount.fromJSONString("EUR:1.5") + Amount.fromJSONString("EUR:1.5")
+ )
+ assertEquals(
+ Amount.fromJSONString("EUR:500000000.00000002"),
+ Amount.fromJSONString("EUR:500000000.00000001") + Amount.fromJSONString("EUR:0.00000001")
+ )
+ assertThrows<AmountOverflowException>("addition didn't overflow") {
+ Amount.fromJSONString("EUR:4503599627370496.99999999") + Amount.fromJSONString("EUR:0.00000001")
+ }
+ assertThrows<AmountOverflowException>("addition didn't overflow") {
+ Amount.fromJSONString("EUR:4000000000000000") + Amount.fromJSONString("EUR:4000000000000000")
+ }
+ }
+
+ @Test
+ fun `test times`() {
+ assertEquals(
+ Amount.fromJSONString("EUR:2"),
+ Amount.fromJSONString("EUR:2") * 1
+ )
+ assertEquals(
+ Amount.fromJSONString("EUR:2"),
+ Amount.fromJSONString("EUR:1") * 2
+ )
+ assertEquals(
+ Amount.fromJSONString("EUR:4.5"),
+ Amount.fromJSONString("EUR:1.5") * 3
+ )
+ assertEquals(
+ Amount.fromJSONString("EUR:1500000000.00000003"),
+ Amount.fromJSONString("EUR:500000000.00000001") * 3
+ )
+ assertThrows<AmountOverflowException>("times didn't overflow") {
+ Amount.fromJSONString("EUR:4000000000000000") * 2
+ }
+ }
+
+ @Test
+ fun `test subtraction`() {
+ assertEquals(
+ Amount.fromJSONString("EUR:0"),
+ Amount.fromJSONString("EUR:1") - Amount.fromJSONString("EUR:1")
+ )
+ assertEquals(
+ Amount.fromJSONString("EUR:1.5"),
+ Amount.fromJSONString("EUR:3") - Amount.fromJSONString("EUR:1.5")
+ )
+ assertEquals(
+ Amount.fromJSONString("EUR:500000000.00000001"),
+ Amount.fromJSONString("EUR:500000000.00000002") - Amount.fromJSONString("EUR:0.00000001")
+ )
+ assertThrows<AmountOverflowException>("subtraction didn't underflow") {
+ Amount.fromJSONString("EUR:23.42") - Amount.fromJSONString("EUR:42.23")
+ }
+ assertThrows<AmountOverflowException>("subtraction didn't underflow") {
+ Amount.fromJSONString("EUR:0.5") - Amount.fromJSONString("EUR:0.50000001")
+ }
+ }
+
+ @Test
+ fun `test isZero()`() {
+ assertTrue(Amount.zero("EUR").isZero())
+ assertTrue(Amount.fromJSONString("EUR:0").isZero())
+ assertTrue(Amount.fromJSONString("EUR:0.0").isZero())
+ assertTrue(Amount.fromJSONString("EUR:0.00000").isZero())
+ assertTrue((Amount.fromJSONString("EUR:1.001") - Amount.fromJSONString("EUR:1.001")).isZero())
+
+ assertFalse(Amount.fromJSONString("EUR:0.00000001").isZero())
+ assertFalse(Amount.fromJSONString("EUR:1.0").isZero())
+ assertFalse(Amount.fromJSONString("EUR:0001.0").isZero())
+ }
+
+ private inline fun <reified T : Throwable> assertThrows(
+ msg: String? = null,
+ function: () -> Any
+ ) {
+ try {
+ function.invoke()
+ fail(msg)
+ } catch (e: Exception) {
+ assertTrue(e is T)
+ }
+ }
+
+}