AmountTest.kt (15052B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2020 Taler Systems S.A. 4 * 5 * GNU Taler is free software; you can redistribute it and/or modify it under the 6 * terms of the GNU General Public License as published by the Free Software 7 * Foundation; either version 3, or (at your option) any later version. 8 * 9 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with 14 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 package net.taler.common 18 19 import android.os.Build 20 import org.junit.Assert.assertEquals 21 import org.junit.Assert.assertFalse 22 import org.junit.Assert.assertTrue 23 import org.junit.Test 24 import java.text.DecimalFormatSymbols 25 26 class AmountTest { 27 28 @Test 29 fun testFromJSONString() { 30 var str = "TESTKUDOS:23.42" 31 var amount = Amount.fromJSONString(str) 32 assertEquals(str, amount.toJSONString()) 33 assertEquals("TESTKUDOS", amount.currency) 34 assertEquals(23, amount.value) 35 assertEquals((0.42 * 1e8).toInt(), amount.fraction) 36 37 str = "EUR:500000000.00000001" 38 amount = Amount.fromJSONString(str) 39 assertEquals(str, amount.toJSONString()) 40 assertEquals("EUR", amount.currency) 41 assertEquals(500000000, amount.value) 42 assertEquals(1, amount.fraction) 43 44 str = "EUR:1500000000.00000003" 45 amount = Amount.fromJSONString(str) 46 assertEquals(str, amount.toJSONString()) 47 assertEquals("EUR", amount.currency) 48 assertEquals(1500000000, amount.value) 49 assertEquals(3, amount.fraction) 50 } 51 52 @Test 53 fun testToString() { 54 amountToString( 55 amount = Amount.fromString("KUDOS", "13.71"), 56 spec = CurrencySpecification( 57 name = "Test (Taler Demostrator)", 58 numFractionalInputDigits = 2, 59 numFractionalNormalDigits = 2, 60 numFractionalTrailingZeroDigits = 2, 61 altUnitNames = mapOf(0 to "ク"), 62 ), 63 rawStr = "13.71", 64 fraction = 71000000, 65 specAmount = "13.71", 66 noSpecAmount = "13.71", 67 currency = "KUDOS", 68 symbol = "ク", 69 ) 70 71 amountToString( 72 amount = Amount.fromString("TESTKUDOS", "23.42"), 73 spec = CurrencySpecification( 74 name = "Test (Taler Unstable Demostrator)", 75 numFractionalInputDigits = 0, 76 numFractionalNormalDigits = 0, 77 numFractionalTrailingZeroDigits = 0, 78 altUnitNames = mapOf(0 to "テ"), 79 ), 80 rawStr = "23.42", 81 fraction = 42000000, 82 specAmount = "23.42", 83 noSpecAmount = "23.42", 84 currency = "TESTKUDOS", 85 symbol = "テ", 86 ) 87 88 amountToString( 89 amount = Amount.fromString("TESTKUDOS", "23"), 90 spec = CurrencySpecification( 91 name = "Test (Taler Unstable Demostrator)", 92 numFractionalInputDigits = 0, 93 numFractionalNormalDigits = 0, 94 numFractionalTrailingZeroDigits = 0, 95 altUnitNames = mapOf(0 to "テ"), 96 ), 97 rawStr = "23", 98 fraction = 0, 99 specAmount = "23", 100 noSpecAmount = "23.00", 101 currency = "TESTKUDOS", 102 symbol = "テ", 103 ) 104 105 amountToString( 106 amount = Amount.fromString("BITCOINBTC", "0.00000001"), 107 spec = CurrencySpecification( 108 name = "Bitcoin", 109 numFractionalInputDigits = 8, 110 numFractionalNormalDigits = 8, 111 numFractionalTrailingZeroDigits = 0, 112 altUnitNames = mapOf( 113 0 to "₿", 114 // TODO: uncomment when units get implemented 115 // and then write tests for units, please 116 // -1 to "d₿", 117 // -2 to "c₿", 118 // -3 to "m₿", 119 // -6 to "µ₿", 120 // -8 to "sat", 121 ), 122 ), 123 rawStr = "0.00000001", 124 fraction = 1, 125 specAmount = "0.00000001", 126 noSpecAmount = "0.00000001", 127 currency = "BITCOINBTC", 128 symbol = "₿", 129 ) 130 131 val specEUR = CurrencySpecification( 132 name = "EUR", 133 numFractionalInputDigits = 2, 134 numFractionalNormalDigits = 2, 135 numFractionalTrailingZeroDigits = 2, 136 altUnitNames = mapOf(0 to "€"), 137 ) 138 139 amountToString( 140 amount = Amount.fromString("EUR", "1500000000.00000003"), 141 spec = specEUR, 142 rawStr = "1500000000.00000003", 143 fraction = 3, 144 specAmount = "1,500,000,000.00000003", 145 noSpecAmount = "1,500,000,000.00000003", 146 currency = "EUR", 147 symbol = "€", 148 ) 149 150 amountToString( 151 amount = Amount.fromString("EUR", "500000000.126"), 152 spec = specEUR, 153 rawStr = "500000000.126", 154 fraction = 12600000, 155 specAmount = "500,000,000.126", 156 noSpecAmount = "500,000,000.126", 157 currency = "EUR", 158 symbol = "€", 159 ) 160 161 amountToString( 162 amount = Amount.fromString("NOSYMBOL", "13.24"), 163 spec = CurrencySpecification( 164 name = "No symbol!", 165 numFractionalInputDigits = 2, 166 numFractionalNormalDigits = 2, 167 numFractionalTrailingZeroDigits = 2, 168 altUnitNames = mapOf(), 169 ), 170 rawStr = "13.24", 171 fraction = 24000000, 172 specAmount = "13.24", 173 noSpecAmount = "13.24", 174 currency = "NOSYMBOL", 175 symbol = "NOSYMBOL", 176 ) 177 } 178 179 private fun amountToString( 180 amount: Amount, 181 spec: CurrencySpecification, 182 rawStr: String, 183 fraction: Int, 184 specAmount: String, 185 noSpecAmount: String, 186 currency: String, 187 symbol: String, 188 ) { 189 val symbols = DecimalFormatSymbols.getInstance() 190 symbols.decimalSeparator = '.' 191 symbols.groupingSeparator = ',' 192 symbols.monetaryDecimalSeparator = '.' 193 if (Build.VERSION.SDK_INT >= 34) { 194 symbols.monetaryGroupingSeparator = ',' 195 } 196 197 // Only the raw amount 198 assertEquals(rawStr, amount.amountStr) 199 assertEquals(fraction, amount.fraction) 200 201 // The amount without currency spec 202 assertEquals("$noSpecAmount $currency", amount.toString(symbols = symbols)) 203 assertEquals(noSpecAmount, amount.toString(symbols = symbols, showSymbol = false)) 204 assertEquals("-$noSpecAmount $currency", amount.toString(symbols = symbols, negative = true)) 205 assertEquals("-$noSpecAmount", amount.toString(symbols = symbols, showSymbol = false, negative = true)) 206 207 // The amount with currency spec 208 val withSpec = amount.withSpec(spec) 209 assertEquals(specAmount, withSpec.toString(symbols = symbols, showSymbol = false)) 210 assertEquals(specAmount, withSpec.toString(symbols = symbols, showSymbol = false)) 211 assertEquals("-$specAmount", withSpec.toString(symbols = symbols, showSymbol = false, negative = true)) 212 assertEquals("-$specAmount", withSpec.toString(symbols = symbols, showSymbol = false, negative = true)) 213 214 // FIXME: this test is locale-dependent 215 if (spec.symbol != null) { 216 assertEquals("${symbol}$specAmount", withSpec.toString(symbols = symbols)) 217 assertEquals("-${symbol}$specAmount", withSpec.toString(symbols = symbols, negative = true)) 218 } else { 219 assertEquals("$specAmount $currency", withSpec.toString(symbols = symbols)) 220 assertEquals("-$specAmount $currency", withSpec.toString(symbols = symbols, negative = true)) 221 } 222 } 223 224 @Test 225 fun testFromJSONStringAcceptsMaxValuesRejectsAbove() { 226 val maxValue = 4503599627370496 227 val str = "TESTKUDOS123:$maxValue.99999999" 228 val amount = Amount.fromJSONString(str) 229 assertEquals(str, amount.toJSONString()) 230 assertEquals("TESTKUDOS123", amount.currency) 231 assertEquals(maxValue, amount.value) 232 233 // longer currency not accepted 234 assertThrows<AmountParserException>("longer currency was accepted") { 235 Amount.fromJSONString("TESTKUDOS1234:$maxValue.99999999") 236 } 237 238 // max value + 1 not accepted 239 assertThrows<AmountParserException>("max value + 1 was accepted") { 240 Amount.fromJSONString("TESTKUDOS123:${maxValue + 1}.99999999") 241 } 242 243 // max fraction + 1 not accepted 244 assertThrows<AmountParserException>("max fraction + 1 was accepted") { 245 Amount.fromJSONString("TESTKUDOS123:$maxValue.999999990") 246 } 247 } 248 249 @Test 250 fun testFromJSONStringRejections() { 251 assertThrows<AmountParserException> { 252 Amount.fromJSONString("TESTKUDOS:0,5") 253 } 254 assertThrows<AmountParserException> { 255 Amount.fromJSONString("+TESTKUDOS:0.5") 256 } 257 assertThrows<AmountParserException> { 258 Amount.fromJSONString("0.5") 259 } 260 assertThrows<AmountParserException> { 261 Amount.fromJSONString(":0.5") 262 } 263 assertThrows<AmountParserException> { 264 Amount.fromJSONString("EUR::0.5") 265 } 266 assertThrows<AmountParserException> { 267 Amount.fromJSONString("EUR:.5") 268 } 269 } 270 271 @Test 272 fun testAddition() { 273 assertEquals( 274 Amount.fromJSONString("EUR:2"), 275 Amount.fromJSONString("EUR:1") + Amount.fromJSONString("EUR:1") 276 ) 277 assertEquals( 278 Amount.fromJSONString("EUR:3"), 279 Amount.fromJSONString("EUR:1.5") + Amount.fromJSONString("EUR:1.5") 280 ) 281 assertEquals( 282 Amount.fromJSONString("EUR:500000000.00000002"), 283 Amount.fromJSONString("EUR:500000000.00000001") + Amount.fromJSONString("EUR:0.00000001") 284 ) 285 assertThrows<AmountOverflowException>("addition didn't overflow") { 286 Amount.fromJSONString("EUR:4503599627370496.99999999") + Amount.fromJSONString("EUR:0.00000001") 287 } 288 assertThrows<AmountOverflowException>("addition didn't overflow") { 289 Amount.fromJSONString("EUR:4000000000000000") + Amount.fromJSONString("EUR:4000000000000000") 290 } 291 } 292 293 @Test 294 fun testTimes() { 295 assertEquals( 296 Amount.fromJSONString("EUR:2"), 297 Amount.fromJSONString("EUR:2") * 1 298 ) 299 assertEquals( 300 Amount.fromJSONString("EUR:2"), 301 Amount.fromJSONString("EUR:1") * 2 302 ) 303 assertEquals( 304 Amount.fromJSONString("EUR:4.5"), 305 Amount.fromJSONString("EUR:1.5") * 3 306 ) 307 assertEquals(Amount.fromJSONString("EUR:0"), Amount.fromJSONString("EUR:1.11") * 0) 308 assertEquals(Amount.fromJSONString("EUR:1.11"), Amount.fromJSONString("EUR:1.11") * 1) 309 assertEquals(Amount.fromJSONString("EUR:2.22"), Amount.fromJSONString("EUR:1.11") * 2) 310 assertEquals(Amount.fromJSONString("EUR:3.33"), Amount.fromJSONString("EUR:1.11") * 3) 311 assertEquals(Amount.fromJSONString("EUR:4.44"), Amount.fromJSONString("EUR:1.11") * 4) 312 assertEquals(Amount.fromJSONString("EUR:5.55"), Amount.fromJSONString("EUR:1.11") * 5) 313 assertEquals( 314 Amount.fromJSONString("EUR:1500000000.00000003"), 315 Amount.fromJSONString("EUR:500000000.00000001") * 3 316 ) 317 assertThrows<AmountOverflowException>("times didn't overflow") { 318 Amount.fromJSONString("EUR:4000000000000000") * 2 319 } 320 } 321 322 @Test 323 fun testSubtraction() { 324 assertEquals( 325 Amount.fromJSONString("EUR:0"), 326 Amount.fromJSONString("EUR:1") - Amount.fromJSONString("EUR:1") 327 ) 328 assertEquals( 329 Amount.fromJSONString("EUR:1.5"), 330 Amount.fromJSONString("EUR:3") - Amount.fromJSONString("EUR:1.5") 331 ) 332 assertEquals( 333 Amount.fromJSONString("EUR:500000000.00000001"), 334 Amount.fromJSONString("EUR:500000000.00000002") - Amount.fromJSONString("EUR:0.00000001") 335 ) 336 assertThrows<AmountOverflowException>("subtraction didn't underflow") { 337 Amount.fromJSONString("EUR:23.42") - Amount.fromJSONString("EUR:42.23") 338 } 339 assertThrows<AmountOverflowException>("subtraction didn't underflow") { 340 Amount.fromJSONString("EUR:0.5") - Amount.fromJSONString("EUR:0.50000001") 341 } 342 } 343 344 @Test 345 fun testIsZero() { 346 assertTrue(Amount.zero("EUR").isZero()) 347 assertTrue(Amount.fromJSONString("EUR:0").isZero()) 348 assertTrue(Amount.fromJSONString("EUR:0.0").isZero()) 349 assertTrue(Amount.fromJSONString("EUR:0.00000").isZero()) 350 assertTrue((Amount.fromJSONString("EUR:1.001") - Amount.fromJSONString("EUR:1.001")).isZero()) 351 352 assertFalse(Amount.fromJSONString("EUR:0.00000001").isZero()) 353 assertFalse(Amount.fromJSONString("EUR:1.0").isZero()) 354 assertFalse(Amount.fromJSONString("EUR:0001.0").isZero()) 355 } 356 357 @Test 358 fun testComparison() { 359 assertTrue(Amount.fromJSONString("EUR:0") <= Amount.fromJSONString("EUR:0")) 360 assertTrue(Amount.fromJSONString("EUR:0") <= Amount.fromJSONString("EUR:0.00000001")) 361 assertTrue(Amount.fromJSONString("EUR:0") < Amount.fromJSONString("EUR:0.00000001")) 362 assertTrue(Amount.fromJSONString("EUR:0") < Amount.fromJSONString("EUR:1")) 363 assertEquals(Amount.fromJSONString("EUR:0"), Amount.fromJSONString("EUR:0")) 364 assertEquals(Amount.fromJSONString("EUR:42"), Amount.fromJSONString("EUR:42")) 365 assertEquals( 366 Amount.fromJSONString("EUR:42.00000001"), 367 Amount.fromJSONString("EUR:42.00000001") 368 ) 369 assertTrue(Amount.fromJSONString("EUR:42.00000001") >= Amount.fromJSONString("EUR:42.00000001")) 370 assertTrue(Amount.fromJSONString("EUR:42.00000002") >= Amount.fromJSONString("EUR:42.00000001")) 371 assertTrue(Amount.fromJSONString("EUR:42.00000002") > Amount.fromJSONString("EUR:42.00000001")) 372 assertTrue(Amount.fromJSONString("EUR:0.00000002") > Amount.fromJSONString("EUR:0.00000001")) 373 assertTrue(Amount.fromJSONString("EUR:0.00000001") > Amount.fromJSONString("EUR:0")) 374 assertTrue(Amount.fromJSONString("EUR:2") > Amount.fromJSONString("EUR:1")) 375 376 assertThrows<IllegalStateException>("could compare amounts with different currencies") { 377 Amount.fromJSONString("EUR:0.5") < Amount.fromJSONString("USD:0.50000001") 378 } 379 } 380 381 }