taler-ios

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

AmountTests.swift (11459B)


      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 = "GOLDLATINUM:\(maxValue).99999999"        // 11 letters is the longest valid currency
     36         amt = try! Amount(fromString: str)
     37         XCTAssert(str == amt.description)
     38         XCTAssert("GOLDLATINUM" == 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: "GOLDLATINUMS:\(maxValue).99999999"))
     48         XCTAssertThrowsError(try Amount(fromString: "TESTKUDOS1:1"))       // letters only
     49         XCTAssertThrowsError(try Amount(fromString: "GOLDLATINUM:\(maxValue + 1).99999999"))
     50         XCTAssertThrowsError(try Amount(fromString: "GOLDLATINUM:\(maxValue).999999990"))
     51         XCTAssertThrowsError(try Amount(fromString: "TESTKUDOS:0,5"))
     52         XCTAssertThrowsError(try Amount(fromString: "+TESTKUDOS:0.5"))
     53         XCTAssertThrowsError(try Amount(fromString: "0.5"))
     54         XCTAssertThrowsError(try Amount(fromString: ":0.5"))
     55         XCTAssertThrowsError(try Amount(fromString: "EUR::0.5"))
     56         XCTAssertThrowsError(try Amount(fromString: "EUR:.5"))
     57     }
     58     
     59     func testAddition() {
     60         XCTAssert(try Amount(fromString: "EUR:1") + Amount(fromString: "EUR:1") == Amount(fromString: "EUR:2"))
     61         XCTAssert(try Amount(fromString: "EUR:1.5") + Amount(fromString: "EUR:1.5") == Amount(fromString: "EUR:3"))
     62         XCTAssert(try Amount(fromString: "EUR:500000000.00000001") + Amount(fromString: "EUR:0.00000001") == Amount(fromString: "EUR:500000000.00000002"))
     63         XCTAssertThrowsError(try Amount(fromString: "EUR:1") + Amount(fromString: "USD:1"))
     64         XCTAssertThrowsError(try Amount(fromString: "EUR:4503599627370496.99999999") + Amount(fromString: "EUR:4503599627370496.99999999"))
     65         XCTAssertThrowsError(try Amount(fromString: "EUR:4000000000000000") + Amount(fromString: "EUR:4000000000000000"))
     66     }
     67     
     68     func testSubtraction() {
     69         XCTAssert(try Amount(fromString: "EUR:1") - Amount(fromString: "EUR:1") == Amount(fromString: "EUR:0"))
     70         XCTAssert(try Amount(fromString: "EUR:3") - Amount(fromString: "EUR:1.5") == Amount(fromString: "EUR:1.5"))
     71         XCTAssert(try Amount(fromString: "EUR:500000000.00000002") - Amount(fromString: "EUR:0.00000001") == Amount(fromString: "EUR:500000000.00000001"))
     72         XCTAssertThrowsError(try Amount(fromString: "EUR:3") - Amount(fromString: "USD:1.5"))
     73         XCTAssertThrowsError(try Amount(fromString: "EUR:23.42") - Amount(fromString: "EUR:42.23"))
     74         XCTAssertThrowsError(try Amount(fromString: "EUR:0.5") - Amount(fromString: "EUR:0.50000001"))
     75     }
     76     
     77     func testMultiplication() {
     78         XCTAssert(try Amount(fromString: "EUR:2") * 1 == Amount(fromString: "EUR:2"))
     79         XCTAssert(try Amount(fromString: "EUR:1") * 2 == Amount(fromString: "EUR:2"))
     80         XCTAssert(try Amount(fromString: "EUR:1.5") * 3 == Amount(fromString: "EUR:4.5"))
     81         XCTAssert(try Amount(fromString: "EUR:1.11") * 0 == Amount(fromString: "EUR:0"))
     82         XCTAssert(try Amount(fromString: "EUR:1.11") * 1 == Amount(fromString: "EUR:1.11"))
     83         XCTAssert(try Amount(fromString: "EUR:1.11") * 2 == Amount(fromString: "EUR:2.22"))
     84         XCTAssert(try Amount(fromString: "EUR:1.11") * 3 == Amount(fromString: "EUR:3.33"))
     85         XCTAssert(try Amount(fromString: "EUR:1.11") * 4 == Amount(fromString: "EUR:4.44"))
     86         XCTAssert(try Amount(fromString: "EUR:1.11") * 5 == Amount(fromString: "EUR:5.55"))
     87         XCTAssert(try Amount(fromString: "EUR:500000000.00000001") * 3 == Amount(fromString: "EUR:1500000000.00000003"))
     88         XCTAssertThrowsError(try Amount(fromString: "4000000000000000") * 2)
     89     }
     90 
     91     func testMultiplicationOverflow() {
     92         // 2^52 is the largest integer part the protocol allows, so it can be parsed and
     93         // then multiplied. Neither the overflow of `integer * factor` nor a product that
     94         // is merely out of range may escape as a trap or as an invalid Amount.
     95         let maxValue = 4503599627370496                 // 2^52
     96         XCTAssertThrowsError(try Amount(fromString: "EUR:\(maxValue)") * 2)
     97         XCTAssertThrowsError(try Amount(fromString: "EUR:\(maxValue)") * 5000)
     98         XCTAssertThrowsError(try Amount(fromString: "EUR:\(maxValue).99999999") * UInt32.max)
     99         XCTAssertThrowsError(try Amount(fromString: "EUR:2251799813685249") * 2)
    100 
    101         // the carry from the fraction still works, and the result is normalized
    102         XCTAssert(try Amount(fromString: "EUR:0.00000001") * UInt32.max == Amount(fromString: "EUR:42.94967295"))
    103         XCTAssert(try Amount(fromString: "EUR:2251799813685248") * 2 == Amount(fromString: "EUR:\(maxValue)"))
    104     }
    105     
    106     func testDivision() {
    107         XCTAssert(try Amount(fromString: "EUR:2") / 1 == Amount(fromString: "EUR:2"))
    108         XCTAssert(try Amount(fromString: "EUR:2") / 2 == Amount(fromString: "EUR:1"))
    109         XCTAssert(try Amount(fromString: "EUR:4.5") / 3 == Amount(fromString: "EUR:1.5"))
    110         XCTAssert(try Amount(fromString: "EUR:0") / 5 == Amount(fromString: "EUR:0"))
    111         XCTAssert(try Amount(fromString: "EUR:1.11") / 1 == Amount(fromString: "EUR:1.11"))
    112         XCTAssert(try Amount(fromString: "EUR:2.22") / 2 == Amount(fromString: "EUR:1.11"))
    113         XCTAssert(try Amount(fromString: "EUR:3.33") / 3 == Amount(fromString: "EUR:1.11"))
    114         XCTAssert(try Amount(fromString: "EUR:4.44") / 4 == Amount(fromString: "EUR:1.11"))
    115         XCTAssert(try Amount(fromString: "EUR:5.55") / 5 == Amount(fromString: "EUR:1.11"))
    116         XCTAssert(try Amount(fromString: "EUR:1500000000.00000003") / 3 == Amount(fromString: "EUR:500000000.00000001"))
    117         XCTAssert(try Amount(fromString: "EUR:0.00000003") / 2 == Amount(fromString: "EUR:0.00000001"))
    118         XCTAssertThrowsError(try Amount(fromString: "EUR:1") / 0)
    119     }
    120     
    121     func testZero() {
    122         XCTAssert(try Amount(fromString: "EUR") == Amount.zero(currency: "EUR"))
    123         XCTAssert(Amount.zero(currency: "EUR").isZero)
    124         XCTAssert(try Amount(fromString: "EUR:0").isZero)
    125         XCTAssert(try Amount(fromString: "EUR:0.0").isZero)
    126         XCTAssert(try Amount(fromString: "EUR:0.00000").isZero)
    127         XCTAssert(try (Amount(fromString: "EUR:1.001") - Amount(fromString: "EUR:1.001")).isZero)
    128         XCTAssert(try !Amount(fromString: "EUR:0.00000001").isZero)
    129         XCTAssert(try !Amount(fromString: "EUR:1.0").isZero)
    130         XCTAssert(try !Amount(fromString: "EUR:0001.0").isZero)
    131     }
    132     
    133     func testComparison() {
    134         XCTAssert(try Amount(fromString: "EUR:0") <= Amount(fromString: "EUR:0"))
    135         XCTAssert(try Amount(fromString: "EUR:0") <= Amount(fromString: "EUR:0.00000001"))
    136         XCTAssert(try Amount(fromString: "EUR:0") < Amount(fromString: "EUR:0.00000001"))
    137         XCTAssert(try Amount(fromString: "EUR:0") < Amount(fromString: "EUR:1"))
    138         XCTAssert(try Amount(fromString: "EUR:0") == Amount(fromString: "EUR:0"))
    139         XCTAssert(try Amount(fromString: "EUR:42") == Amount(fromString: "EUR:42"))
    140         XCTAssert(try Amount(fromString: "EUR:42.00000001") == Amount(fromString: "EUR:42.00000001"))
    141         XCTAssert(try Amount(fromString: "EUR:42.00000001") >= Amount(fromString: "EUR:42.00000001"))
    142         XCTAssert(try Amount(fromString: "EUR:42.00000002") >= Amount(fromString: "EUR:42.00000001"))
    143         XCTAssert(try Amount(fromString: "EUR:42.00000002") > Amount(fromString: "EUR:42.00000001"))
    144         XCTAssert(try Amount(fromString: "EUR:0.00000002") > Amount(fromString: "EUR:0.00000001"))
    145         XCTAssert(try Amount(fromString: "EUR:0.00000001") > Amount(fromString: "EUR:0"))
    146         XCTAssert(try Amount(fromString: "EUR:2") > Amount(fromString: "EUR:1"))
    147         XCTAssertThrowsError(try Amount(fromString: "EUR:0.5") < Amount(fromString: "USD:0.50000001"))
    148     }
    149 }
    150 
    151 // REL-001: the currency field formats after every digit, including rejected edits.
    152 extension AmountTests {
    153     func testDigitEntryRemainsRepresentableAtEveryPrecision() throws {
    154         for digits in UInt(0)...8 {
    155             let amount = Amount.zero(currency: "EUR")
    156             for _ in 0..<40 {
    157                 amount.shiftLeft(add: 9, digits)
    158                 XCTAssertLessThanOrEqual(amount.integer, 4_503_599_627_370_496)
    159                 XCTAssertLessThan(amount.fraction, 100_000_000)
    160                 _ = amount.plainString(inputDigits: digits)
    161                 XCTAssertEqual(try Amount(fromString: amount.description), amount)
    162             }
    163             let before = amount.copy()
    164             amount.shiftLeft(add: 9, digits)
    165             XCTAssertEqual(amount, before)
    166             amount.shiftRight()
    167             XCTAssertTrue(try amount < before)
    168             amount.shiftLeft(add: 1, digits)
    169             _ = amount.plainString(inputDigits: digits)
    170         }
    171     }
    172 
    173     func testEighteenthDigitIsRejectedAtomically() throws {
    174         let amount = Amount.zero(currency: "EUR")
    175         for _ in 0..<17 { amount.shiftLeft(add: 9, 2) }
    176         XCTAssertEqual(amount.description, "EUR:999999999999999.99")
    177         amount.shiftLeft(add: 9, 2)
    178         XCTAssertEqual(amount.plainString(inputDigits: 2), "99999999999999999")
    179     }
    180 
    181     func testMaximumAmountEditPreservesValue() throws {
    182         for digits in UInt(0)...8 {
    183             let amount = try Amount(fromString: "EUR:4503599627370496.99999999")
    184             let before = amount.copy()
    185             amount.shiftLeft(add: 0, digits)
    186             XCTAssertEqual(amount, before)
    187         }
    188         let amount = try Amount(fromString: "EUR:450359962737049")
    189         amount.shiftLeft(add: 6, 0)
    190         XCTAssertEqual(amount.integer, 4_503_599_627_370_496)
    191     }
    192 
    193     func testPlainStringNormalizesWideFractionWithoutOverflow() {
    194         let amount = Amount(currency: "EUR", integer: 0, fraction: 909_000_000)
    195         XCTAssertEqual(amount.plainString(inputDigits: 2), "909")
    196     }
    197 }