AmountTests.swift (9288B)
1 /* 2 * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 import XCTest 6 @testable import taler_swift 7 8 class AmountTests: XCTestCase { 9 func testParsing() { 10 var str = "TESTKUDOS:23.42" 11 var amt = try! Amount(fromString: str) 12 XCTAssert(str == amt.description) 13 XCTAssert("TESTKUDOS" == amt.currencyStr) 14 // `value` is a lossy Double convenience (23.42 here), never the integer part. 15 // Assert on the exact integer/fraction pair instead — money must not be compared 16 // through Double. 17 XCTAssert(23 == amt.integer) 18 XCTAssert(UInt64(0.42 * 1e8) == amt.fraction) 19 20 str = "EUR:500000000.00000001" 21 amt = try! Amount(fromString: str) 22 XCTAssert(str == amt.description) 23 XCTAssert("EUR" == amt.currencyStr) 24 XCTAssert(500000000 == amt.value) 25 XCTAssert(1 == amt.fraction) 26 27 str = "EUR:1500000000.00000003" 28 amt = try! Amount(fromString: str) 29 XCTAssert(str == amt.description) 30 XCTAssert("EUR" == amt.currencyStr) 31 XCTAssert(1500000000 == amt.value) 32 XCTAssert(3 == amt.fraction) 33 34 let maxValue = 4503599627370496 // 16 significant digits are 1 too many for double 35 str = "TESTKUDOS123:\(maxValue).99999999" 36 amt = try! Amount(fromString: str) 37 XCTAssert(str == amt.description) 38 XCTAssert("TESTKUDOS123" == amt.currencyStr) 39 // At 2^52 the spacing of Double is 1.0, so `value` cannot represent 40 // maxValue + 0.99999999 — it rounds up to maxValue + 1. That is exactly the 41 // precision loss the `maxValue` comment warns about, so assert on the exact 42 // fields and pin the lossiness rather than asserting a false identity. 43 XCTAssert(UInt64(maxValue) == amt.integer) 44 XCTAssert(99999999 == amt.fraction) 45 XCTAssert(Double(maxValue) != amt.value) 46 47 XCTAssertThrowsError(try Amount(fromString: "TESTKUDOS1234:\(maxValue).99999999")) 48 XCTAssertThrowsError(try Amount(fromString: "TESTKUDOS123:\(maxValue + 1).99999999")) 49 XCTAssertThrowsError(try Amount(fromString: "TESTKUDOS123:\(maxValue).999999990")) 50 XCTAssertThrowsError(try Amount(fromString: "TESTKUDOS:0,5")) 51 XCTAssertThrowsError(try Amount(fromString: "+TESTKUDOS:0.5")) 52 XCTAssertThrowsError(try Amount(fromString: "0.5")) 53 XCTAssertThrowsError(try Amount(fromString: ":0.5")) 54 XCTAssertThrowsError(try Amount(fromString: "EUR::0.5")) 55 XCTAssertThrowsError(try Amount(fromString: "EUR:.5")) 56 } 57 58 func testAddition() { 59 XCTAssert(try Amount(fromString: "EUR:1") + Amount(fromString: "EUR:1") == Amount(fromString: "EUR:2")) 60 XCTAssert(try Amount(fromString: "EUR:1.5") + Amount(fromString: "EUR:1.5") == Amount(fromString: "EUR:3")) 61 XCTAssert(try Amount(fromString: "EUR:500000000.00000001") + Amount(fromString: "EUR:0.00000001") == Amount(fromString: "EUR:500000000.00000002")) 62 XCTAssertThrowsError(try Amount(fromString: "EUR:1") + Amount(fromString: "USD:1")) 63 XCTAssertThrowsError(try Amount(fromString: "EUR:4503599627370496.99999999") + Amount(fromString: "EUR:4503599627370496.99999999")) 64 XCTAssertThrowsError(try Amount(fromString: "EUR:4000000000000000") + Amount(fromString: "EUR:4000000000000000")) 65 } 66 67 func testSubtraction() { 68 XCTAssert(try Amount(fromString: "EUR:1") - Amount(fromString: "EUR:1") == Amount(fromString: "EUR:0")) 69 XCTAssert(try Amount(fromString: "EUR:3") - Amount(fromString: "EUR:1.5") == Amount(fromString: "EUR:1.5")) 70 XCTAssert(try Amount(fromString: "EUR:500000000.00000002") - Amount(fromString: "EUR:0.00000001") == Amount(fromString: "EUR:500000000.00000001")) 71 XCTAssertThrowsError(try Amount(fromString: "EUR:3") - Amount(fromString: "USD:1.5")) 72 XCTAssertThrowsError(try Amount(fromString: "EUR:23.42") - Amount(fromString: "EUR:42.23")) 73 XCTAssertThrowsError(try Amount(fromString: "EUR:0.5") - Amount(fromString: "EUR:0.50000001")) 74 } 75 76 func testMultiplication() { 77 XCTAssert(try Amount(fromString: "EUR:2") * 1 == Amount(fromString: "EUR:2")) 78 XCTAssert(try Amount(fromString: "EUR:1") * 2 == Amount(fromString: "EUR:2")) 79 XCTAssert(try Amount(fromString: "EUR:1.5") * 3 == Amount(fromString: "EUR:4.5")) 80 XCTAssert(try Amount(fromString: "EUR:1.11") * 0 == Amount(fromString: "EUR:0")) 81 XCTAssert(try Amount(fromString: "EUR:1.11") * 1 == Amount(fromString: "EUR:1.11")) 82 XCTAssert(try Amount(fromString: "EUR:1.11") * 2 == Amount(fromString: "EUR:2.22")) 83 XCTAssert(try Amount(fromString: "EUR:1.11") * 3 == Amount(fromString: "EUR:3.33")) 84 XCTAssert(try Amount(fromString: "EUR:1.11") * 4 == Amount(fromString: "EUR:4.44")) 85 XCTAssert(try Amount(fromString: "EUR:1.11") * 5 == Amount(fromString: "EUR:5.55")) 86 XCTAssert(try Amount(fromString: "EUR:500000000.00000001") * 3 == Amount(fromString: "EUR:1500000000.00000003")) 87 XCTAssertThrowsError(try Amount(fromString: "4000000000000000") * 2) 88 } 89 90 func testMultiplicationOverflow() { 91 // 2^52 is the largest integer part the protocol allows, so it can be parsed and 92 // then multiplied. Neither the overflow of `integer * factor` nor a product that 93 // is merely out of range may escape as a trap or as an invalid Amount. 94 let maxValue = 4503599627370496 // 2^52 95 XCTAssertThrowsError(try Amount(fromString: "EUR:\(maxValue)") * 2) 96 XCTAssertThrowsError(try Amount(fromString: "EUR:\(maxValue)") * 5000) 97 XCTAssertThrowsError(try Amount(fromString: "EUR:\(maxValue).99999999") * UInt32.max) 98 XCTAssertThrowsError(try Amount(fromString: "EUR:2251799813685249") * 2) 99 100 // the carry from the fraction still works, and the result is normalized 101 XCTAssert(try Amount(fromString: "EUR:0.00000001") * UInt32.max == Amount(fromString: "EUR:42.94967295")) 102 XCTAssert(try Amount(fromString: "EUR:2251799813685248") * 2 == Amount(fromString: "EUR:\(maxValue)")) 103 } 104 105 func testDivision() { 106 XCTAssert(try Amount(fromString: "EUR:2") / 1 == Amount(fromString: "EUR:2")) 107 XCTAssert(try Amount(fromString: "EUR:2") / 2 == Amount(fromString: "EUR:1")) 108 XCTAssert(try Amount(fromString: "EUR:4.5") / 3 == Amount(fromString: "EUR:1.5")) 109 XCTAssert(try Amount(fromString: "EUR:0") / 5 == Amount(fromString: "EUR:0")) 110 XCTAssert(try Amount(fromString: "EUR:1.11") / 1 == Amount(fromString: "EUR:1.11")) 111 XCTAssert(try Amount(fromString: "EUR:2.22") / 2 == Amount(fromString: "EUR:1.11")) 112 XCTAssert(try Amount(fromString: "EUR:3.33") / 3 == Amount(fromString: "EUR:1.11")) 113 XCTAssert(try Amount(fromString: "EUR:4.44") / 4 == Amount(fromString: "EUR:1.11")) 114 XCTAssert(try Amount(fromString: "EUR:5.55") / 5 == Amount(fromString: "EUR:1.11")) 115 XCTAssert(try Amount(fromString: "EUR:1500000000.00000003") / 3 == Amount(fromString: "EUR:500000000.00000001")) 116 XCTAssert(try Amount(fromString: "EUR:0.00000003") / 2 == Amount(fromString: "EUR:0.00000001")) 117 XCTAssertThrowsError(try Amount(fromString: "EUR:1") / 0) 118 } 119 120 func testZero() { 121 XCTAssert(try Amount(fromString: "EUR") == Amount.zero(currency: "EUR")) 122 XCTAssert(Amount.zero(currency: "EUR").isZero) 123 XCTAssert(try Amount(fromString: "EUR:0").isZero) 124 XCTAssert(try Amount(fromString: "EUR:0.0").isZero) 125 XCTAssert(try Amount(fromString: "EUR:0.00000").isZero) 126 XCTAssert(try (Amount(fromString: "EUR:1.001") - Amount(fromString: "EUR:1.001")).isZero) 127 XCTAssert(try !Amount(fromString: "EUR:0.00000001").isZero) 128 XCTAssert(try !Amount(fromString: "EUR:1.0").isZero) 129 XCTAssert(try !Amount(fromString: "EUR:0001.0").isZero) 130 } 131 132 func testComparison() { 133 XCTAssert(try Amount(fromString: "EUR:0") <= Amount(fromString: "EUR:0")) 134 XCTAssert(try Amount(fromString: "EUR:0") <= Amount(fromString: "EUR:0.00000001")) 135 XCTAssert(try Amount(fromString: "EUR:0") < Amount(fromString: "EUR:0.00000001")) 136 XCTAssert(try Amount(fromString: "EUR:0") < Amount(fromString: "EUR:1")) 137 XCTAssert(try Amount(fromString: "EUR:0") == Amount(fromString: "EUR:0")) 138 XCTAssert(try Amount(fromString: "EUR:42") == Amount(fromString: "EUR:42")) 139 XCTAssert(try Amount(fromString: "EUR:42.00000001") == Amount(fromString: "EUR:42.00000001")) 140 XCTAssert(try Amount(fromString: "EUR:42.00000001") >= Amount(fromString: "EUR:42.00000001")) 141 XCTAssert(try Amount(fromString: "EUR:42.00000002") >= Amount(fromString: "EUR:42.00000001")) 142 XCTAssert(try Amount(fromString: "EUR:42.00000002") > Amount(fromString: "EUR:42.00000001")) 143 XCTAssert(try Amount(fromString: "EUR:0.00000002") > Amount(fromString: "EUR:0.00000001")) 144 XCTAssert(try Amount(fromString: "EUR:0.00000001") > Amount(fromString: "EUR:0")) 145 XCTAssert(try Amount(fromString: "EUR:2") > Amount(fromString: "EUR:1")) 146 XCTAssertThrowsError(try Amount(fromString: "EUR:0.5") < Amount(fromString: "USD:0.50000001")) 147 } 148 }