/*
* 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 android.os.Build
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import java.text.DecimalFormatSymbols
import kotlin.random.Random
class AmountTest {
companion object {
fun getRandomAmount() = getRandomAmount(getRandomString(1, Random.nextInt(1, 12)))
fun getRandomAmount(currency: String): Amount {
val value = Random.nextLong(0, Amount.MAX_VALUE)
val fraction = Random.nextInt(0, Amount.MAX_FRACTION)
return Amount(currency, value, fraction)
}
}
@Test
fun testFromJSONString() {
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)
str = "EUR:500000000.00000001"
amount = Amount.fromJSONString(str)
assertEquals(str, amount.toJSONString())
assertEquals("EUR", amount.currency)
assertEquals(500000000, amount.value)
assertEquals(1, amount.fraction)
str = "EUR:1500000000.00000003"
amount = Amount.fromJSONString(str)
assertEquals(str, amount.toJSONString())
assertEquals("EUR", amount.currency)
assertEquals(1500000000, amount.value)
assertEquals(3, amount.fraction)
}
@Test
fun testToString() {
amountToString(
amount = Amount.fromString("KUDOS", "13.71"),
spec = CurrencySpecification(
name = "Test (Taler Demostrator)",
numFractionalInputDigits = 2,
numFractionalNormalDigits = 2,
numFractionalTrailingZeroDigits = 2,
altUnitNames = mapOf(0 to "ク"),
),
rawStr = "13.71",
fraction = 71000000,
specAmount = "13.71",
noSpecAmount = "13.71",
currency = "KUDOS",
symbol = "ク",
)
amountToString(
amount = Amount.fromString("TESTKUDOS", "23.42"),
spec = CurrencySpecification(
name = "Test (Taler Unstable Demostrator)",
numFractionalInputDigits = 0,
numFractionalNormalDigits = 0,
numFractionalTrailingZeroDigits = 0,
altUnitNames = mapOf(0 to "テ"),
),
rawStr = "23.42",
fraction = 42000000,
specAmount = "23",
noSpecAmount = "23.42",
currency = "TESTKUDOS",
symbol = "テ",
)
amountToString(
amount = Amount.fromString("BITCOINBTC", "0.00000001"),
spec = CurrencySpecification(
name = "Bitcoin",
numFractionalInputDigits = 8,
numFractionalNormalDigits = 8,
numFractionalTrailingZeroDigits = 0,
altUnitNames = mapOf(
0 to "₿",
// TODO: uncomment when units get implemented
// and then write tests for units, please
// -1 to "d₿",
// -2 to "c₿",
// -3 to "m₿",
// -6 to "µ₿",
// -8 to "sat",
),
),
rawStr = "0.00000001",
fraction = 1,
specAmount = "0.00000001",
noSpecAmount = "0.00000001",
currency = "BITCOINBTC",
symbol = "₿",
)
val specEUR = CurrencySpecification(
name = "EUR",
numFractionalInputDigits = 2,
numFractionalNormalDigits = 2,
numFractionalTrailingZeroDigits = 2,
altUnitNames = mapOf(0 to "€"),
)
amountToString(
amount = Amount.fromString("EUR", "1500000000.00000003"),
spec = specEUR,
rawStr = "1500000000.00000003",
fraction = 3,
specAmount = "1,500,000,000.00",
noSpecAmount = "1,500,000,000.00000003",
currency = "EUR",
symbol = "€",
)
amountToString(
amount = Amount.fromString("EUR", "500000000.126"),
spec = specEUR,
rawStr = "500000000.126",
fraction = 12600000,
specAmount = "500,000,000.13",
noSpecAmount = "500,000,000.126",
currency = "EUR",
symbol = "€",
)
amountToString(
amount = Amount.fromString("NOSYMBOL", "13.24"),
spec = CurrencySpecification(
name = "No symbol!",
numFractionalInputDigits = 2,
numFractionalNormalDigits = 2,
numFractionalTrailingZeroDigits = 2,
altUnitNames = mapOf(),
),
rawStr = "13.24",
fraction = 24000000,
specAmount = "13.24",
noSpecAmount = "13.24",
currency = "NOSYMBOL",
symbol = "NOSYMBOL",
)
}
private fun amountToString(
amount: Amount,
spec: CurrencySpecification,
rawStr: String,
fraction: Int,
specAmount: String,
noSpecAmount: String,
currency: String,
symbol: String,
) {
val symbols = DecimalFormatSymbols.getInstance()
symbols.decimalSeparator = '.'
symbols.groupingSeparator = ','
symbols.monetaryDecimalSeparator = '.'
if (Build.VERSION.SDK_INT >= 34) {
symbols.monetaryGroupingSeparator = ','
}
// Only the raw amount
assertEquals(rawStr, amount.amountStr)
assertEquals(fraction, amount.fraction)
// The amount without currency spec
assertEquals("$noSpecAmount $currency", amount.toString(symbols = symbols))
assertEquals(noSpecAmount, amount.toString(symbols = symbols, showSymbol = false))
assertEquals("-$noSpecAmount $currency", amount.toString(symbols = symbols, negative = true))
assertEquals("-$noSpecAmount", amount.toString(symbols = symbols, showSymbol = false, negative = true))
// The amount with currency spec
val withSpec = amount.withSpec(spec)
assertEquals(specAmount, withSpec.toString(symbols = symbols, showSymbol = false))
assertEquals(specAmount, withSpec.toString(symbols = symbols, showSymbol = false))
assertEquals("-$specAmount", withSpec.toString(symbols = symbols, showSymbol = false, negative = true))
assertEquals("-$specAmount", withSpec.toString(symbols = symbols, showSymbol = false, negative = true))
if (spec.symbol != null) {
assertEquals("${symbol}$specAmount", withSpec.toString(symbols = symbols))
assertEquals("-${symbol}$specAmount", withSpec.toString(symbols = symbols, negative = true))
} else {
assertEquals("$specAmount $currency", withSpec.toString(symbols = symbols))
assertEquals("-$specAmount $currency", withSpec.toString(symbols = symbols, negative = true))
}
}
@Test
fun testFromJSONStringAcceptsMaxValuesRejectsAbove() {
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)
// longer currency not accepted
assertThrows("longer currency was accepted") {
Amount.fromJSONString("TESTKUDOS1234:$maxValue.99999999")
}
// max value + 1 not accepted
assertThrows("max value + 1 was accepted") {
Amount.fromJSONString("TESTKUDOS123:${maxValue + 1}.99999999")
}
// max fraction + 1 not accepted
assertThrows("max fraction + 1 was accepted") {
Amount.fromJSONString("TESTKUDOS123:$maxValue.999999990")
}
}
@Test
fun testFromJSONStringRejections() {
assertThrows {
Amount.fromJSONString("TESTKUDOS:0,5")
}
assertThrows {
Amount.fromJSONString("+TESTKUDOS:0.5")
}
assertThrows {
Amount.fromJSONString("0.5")
}
assertThrows {
Amount.fromJSONString(":0.5")
}
assertThrows {
Amount.fromJSONString("EUR::0.5")
}
assertThrows {
Amount.fromJSONString("EUR:.5")
}
}
@Test
fun testAddition() {
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("addition didn't overflow") {
Amount.fromJSONString("EUR:4503599627370496.99999999") + Amount.fromJSONString("EUR:0.00000001")
}
assertThrows("addition didn't overflow") {
Amount.fromJSONString("EUR:4000000000000000") + Amount.fromJSONString("EUR:4000000000000000")
}
}
@Test
fun testTimes() {
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:0"), Amount.fromJSONString("EUR:1.11") * 0)
assertEquals(Amount.fromJSONString("EUR:1.11"), Amount.fromJSONString("EUR:1.11") * 1)
assertEquals(Amount.fromJSONString("EUR:2.22"), Amount.fromJSONString("EUR:1.11") * 2)
assertEquals(Amount.fromJSONString("EUR:3.33"), Amount.fromJSONString("EUR:1.11") * 3)
assertEquals(Amount.fromJSONString("EUR:4.44"), Amount.fromJSONString("EUR:1.11") * 4)
assertEquals(Amount.fromJSONString("EUR:5.55"), Amount.fromJSONString("EUR:1.11") * 5)
assertEquals(
Amount.fromJSONString("EUR:1500000000.00000003"),
Amount.fromJSONString("EUR:500000000.00000001") * 3
)
assertThrows("times didn't overflow") {
Amount.fromJSONString("EUR:4000000000000000") * 2
}
}
@Test
fun testSubtraction() {
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("subtraction didn't underflow") {
Amount.fromJSONString("EUR:23.42") - Amount.fromJSONString("EUR:42.23")
}
assertThrows("subtraction didn't underflow") {
Amount.fromJSONString("EUR:0.5") - Amount.fromJSONString("EUR:0.50000001")
}
}
@Test
fun testIsZero() {
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())
}
@Test
fun testComparison() {
assertTrue(Amount.fromJSONString("EUR:0") <= Amount.fromJSONString("EUR:0"))
assertTrue(Amount.fromJSONString("EUR:0") <= Amount.fromJSONString("EUR:0.00000001"))
assertTrue(Amount.fromJSONString("EUR:0") < Amount.fromJSONString("EUR:0.00000001"))
assertTrue(Amount.fromJSONString("EUR:0") < Amount.fromJSONString("EUR:1"))
assertEquals(Amount.fromJSONString("EUR:0"), Amount.fromJSONString("EUR:0"))
assertEquals(Amount.fromJSONString("EUR:42"), Amount.fromJSONString("EUR:42"))
assertEquals(
Amount.fromJSONString("EUR:42.00000001"),
Amount.fromJSONString("EUR:42.00000001")
)
assertTrue(Amount.fromJSONString("EUR:42.00000001") >= Amount.fromJSONString("EUR:42.00000001"))
assertTrue(Amount.fromJSONString("EUR:42.00000002") >= Amount.fromJSONString("EUR:42.00000001"))
assertTrue(Amount.fromJSONString("EUR:42.00000002") > Amount.fromJSONString("EUR:42.00000001"))
assertTrue(Amount.fromJSONString("EUR:0.00000002") > Amount.fromJSONString("EUR:0.00000001"))
assertTrue(Amount.fromJSONString("EUR:0.00000001") > Amount.fromJSONString("EUR:0"))
assertTrue(Amount.fromJSONString("EUR:2") > Amount.fromJSONString("EUR:1"))
assertThrows("could compare amounts with different currencies") {
Amount.fromJSONString("EUR:0.5") < Amount.fromJSONString("USD:0.50000001")
}
}
}