taler-ios

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

AmountTests.swift (11322B)


      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 }
    149 
    150 // REL-001: the currency field formats after every digit, including rejected edits.
    151 extension AmountTests {
    152     func testDigitEntryRemainsRepresentableAtEveryPrecision() throws {
    153         for digits in UInt(0)...8 {
    154             let amount = Amount.zero(currency: "EUR")
    155             for _ in 0..<40 {
    156                 amount.shiftLeft(add: 9, digits)
    157                 XCTAssertLessThanOrEqual(amount.integer, 4_503_599_627_370_496)
    158                 XCTAssertLessThan(amount.fraction, 100_000_000)
    159                 _ = amount.plainString(inputDigits: digits)
    160                 XCTAssertEqual(try Amount(fromString: amount.description), amount)
    161             }
    162             let before = amount.copy()
    163             amount.shiftLeft(add: 9, digits)
    164             XCTAssertEqual(amount, before)
    165             amount.shiftRight()
    166             XCTAssertTrue(try amount < before)
    167             amount.shiftLeft(add: 1, digits)
    168             _ = amount.plainString(inputDigits: digits)
    169         }
    170     }
    171 
    172     func testEighteenthDigitIsRejectedAtomically() throws {
    173         let amount = Amount.zero(currency: "EUR")
    174         for _ in 0..<17 { amount.shiftLeft(add: 9, 2) }
    175         XCTAssertEqual(amount.description, "EUR:999999999999999.99")
    176         amount.shiftLeft(add: 9, 2)
    177         XCTAssertEqual(amount.plainString(inputDigits: 2), "99999999999999999")
    178     }
    179 
    180     func testMaximumAmountEditPreservesValue() throws {
    181         for digits in UInt(0)...8 {
    182             let amount = try Amount(fromString: "EUR:4503599627370496.99999999")
    183             let before = amount.copy()
    184             amount.shiftLeft(add: 0, digits)
    185             XCTAssertEqual(amount, before)
    186         }
    187         let amount = try Amount(fromString: "EUR:450359962737049")
    188         amount.shiftLeft(add: 6, 0)
    189         XCTAssertEqual(amount.integer, 4_503_599_627_370_496)
    190     }
    191 
    192     func testPlainStringNormalizesWideFractionWithoutOverflow() {
    193         let amount = Amount(currency: "EUR", integer: 0, fraction: 909_000_000)
    194         XCTAssertEqual(amount.plainString(inputDigits: 2), "909")
    195     }
    196 }