taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }