diff options
author | Jonathan Buchanan <jonathan.russ.buchanan@gmail.com> | 2021-08-09 21:22:52 -0400 |
---|---|---|
committer | Jonathan Buchanan <jonathan.russ.buchanan@gmail.com> | 2021-08-09 21:22:52 -0400 |
commit | c98d8fbcbdb1767d9ae2caae3d58d97f06d278d1 (patch) | |
tree | f3d8ce01a5860ca63b093ab4f4327157f65fcbfc /Taler/Amount.swift | |
parent | 62b663428746c82a7eb46ff673364b8a03eea825 (diff) | |
download | taler-ios-c98d8fbcbdb1767d9ae2caae3d58d97f06d278d1.tar.gz taler-ios-c98d8fbcbdb1767d9ae2caae3d58d97f06d278d1.tar.bz2 taler-ios-c98d8fbcbdb1767d9ae2caae3d58d97f06d278d1.zip |
finish implementing, testing, and documenting amounts
Diffstat (limited to 'Taler/Amount.swift')
-rw-r--r-- | Taler/Amount.swift | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/Taler/Amount.swift b/Taler/Amount.swift index 83445b4..98bb1b9 100644 --- a/Taler/Amount.swift +++ b/Taler/Amount.swift @@ -16,20 +16,73 @@ import Foundation +/** + Errors for `Amount`. + */ enum AmountError: Error { + /** + The string cannot be parsed to create an `Amount`. + */ case invalidStringRepresentation + + /** + Could not compare or operate on two `Amount`s of different currencies. + */ case incompatibleCurrency + + /** + The amount is invalid. The value is either greater than the maximum, or the currency is the empty string. + */ case invalidAmount + + /** + The result of the operation would yield a negative amount. + */ case negativeAmount + + /** + The operation was division by zero. + */ + case divideByZero } +/** + A value of a currency. + */ class Amount: Codable, CustomStringConvertible { + /** + The largest possible value that can be represented. + */ private static let maxValue: UInt64 = 1 << 52 + + /** + The size of `value` in relation to `fraction`. + */ private static let fractionalBase: UInt32 = 100000000 + + /** + The greatest number of decimal digits that can be represented. + */ private static let fractionalBaseDigits: UInt = 8 + + /** + The currency of the amount. + */ var currency: String + + /** + The value of the amount (number to the left of the decimal point). + */ var value: UInt64 + + /** + The fractional value of the amount (number to the right of the decimal point). + */ var fraction: UInt32 + + /** + The string representation of the amount, formatted as "`currency`:`value`.`fraction`". + */ var description: String { if fraction == 0 { return "\(currency):\(value)" @@ -43,10 +96,24 @@ class Amount: Codable, CustomStringConvertible { return "\(currency):\(value).\(fracStr)" } } + + /** + Whether the value is valid. An amount is valid if and only if the currency is not empty and the value is less than the maximum allowed value. + */ var valid: Bool { return (value <= Amount.maxValue && currency != "") } + /** + Initializes an amount by parsing a string representing the amount. The string should be formatted as "`currency`:`value`.`fraction`". + + - Parameters: + - fromString: The string to parse. + + - Throws: + - `AmountError.invalidStringRepresentation` if the string cannot be parsed. + - `AmountError.invalidAmount` if the string can be parsed, but the resulting amount is not valid. + */ init(fromString string: String) throws { guard let separatorIndex = string.firstIndex(of: ":") else { throw AmountError.invalidStringRepresentation } self.currency = String(string[..<separatorIndex]) @@ -71,12 +138,30 @@ class Amount: Codable, CustomStringConvertible { guard self.valid else { throw AmountError.invalidAmount } } + /** + Initializes an amount with the specified currency, value, and fraction. + + - Parameters: + - currency: The currency of the amount. + - value: The value of the amount (number to the left of the decimal point). + - fraction: The fractional value of the amount (number to the right of the decimal point). + */ init(currency: String, value: UInt64, fraction: UInt32) { self.currency = currency self.value = value self.fraction = fraction } + /** + Initializes an amount from a decoder. + + - Parameters: + - fromDecoder: The decoder to extract the amount from. + + - Throws: + - `AmountError.invalidStringRepresentation` if the string cannot be parsed. + - `AmountError.invalidAmount` if the string can be parsed, but the resulting amount is not valid. + */ init(fromDecoder decoder: Decoder) throws { let container = try decoder.singleValueContainer() let string = try container.decode(String.self) @@ -104,21 +189,43 @@ class Amount: Codable, CustomStringConvertible { guard self.valid else { throw AmountError.invalidAmount } } + /** + Copies an amount. + + - Returns: A copy of the amount. + */ func copy() -> Amount { return Amount(currency: self.currency, value: self.value, fraction: self.fraction) } + /** + Creates a normalized copy of an amount. + + - Returns: A copy of the amount that has been normalized + */ func normalizedCopy() throws -> Amount { let amount = self.copy() try amount.normalize() return amount } + /** + Encodes an amount. + + - Parameters: + - to: The encoder to encode the amount with. + */ func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(self.description) } + /** + Normalizes an amount by reducing `fraction` until it is less than `Amount.fractionalBase`, increasing `value` appropriately. + + - Throws: + - `AmountError.invalidAmount` if the amount is invalid either before or after normalization. + */ func normalize() throws { if !valid { throw AmountError.invalidAmount @@ -130,6 +237,18 @@ class Amount: Codable, CustomStringConvertible { } } + /** + Adds two amounts together. + + - Parameters: + - left: The amount on the left. + - right: The amount on the right. + + - Throws: + - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. + + - Returns: The sum of `left` and `right`, normalized. + */ static func + (left: Amount, right: Amount) throws -> Amount { if left.currency != right.currency { throw AmountError.incompatibleCurrency @@ -143,6 +262,18 @@ class Amount: Codable, CustomStringConvertible { return result } + /** + Subtracts one amount from another. + + - Parameters: + - left: The amount on the left. + - right: The amount on the right. + + - Throws: + - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. + + - Returns: The difference of `left` and `right`, normalized. + */ static func - (left: Amount, right: Amount) throws -> Amount { if left.currency != right.currency { throw AmountError.incompatibleCurrency @@ -162,6 +293,59 @@ class Amount: Codable, CustomStringConvertible { return diff } + /** + Divides an amount by a scalar, possibly introducing rounding error. + + - Parameters: + - dividend: The amount to divide. + - divisor: The scalar dividing `dividend`. + + - Returns: The quotient of `dividend` and `divisor`, normalized. + */ + static func / (dividend: Amount, divisor: UInt32) throws -> Amount { + guard divisor != 0 else { throw AmountError.divideByZero } + let result = try dividend.normalizedCopy() + if (divisor == 1) { + return result + } + var remainder = result.value % UInt64(divisor) + result.value = result.value / UInt64(divisor) + remainder = (remainder * UInt64(Amount.fractionalBase)) + UInt64(result.fraction) + result.fraction = UInt32(remainder) / divisor + try result.normalize() + return result + } + + /** + Multiply an amount by a scalar. + + - Parameters: + - amount: The amount to multiply. + - factor: The scalar multiplying `amount`. + + - Returns: The product of `amount` and `factor`, normalized. + */ + static func * (amount: Amount, factor: UInt32) throws -> Amount { + let result = try amount.normalizedCopy() + result.value = result.value * UInt64(factor) + let fraction_tmp = UInt64(result.fraction) * UInt64(factor) + result.value += fraction_tmp / UInt64(Amount.fractionalBase) + result.fraction = UInt32(fraction_tmp % UInt64(Amount.fractionalBase)) + return result + } + + /** + Compares two amounts. + + - Parameters: + - left: The first amount. + - right: The second amount. + + - Throws: + - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. + + - Returns: `true` if and only if the amounts have the same `value` and `fraction` after normalization, `false` otherwise. + */ static func == (left: Amount, right: Amount) throws -> Bool { if left.currency != right.currency { throw AmountError.incompatibleCurrency @@ -171,6 +355,18 @@ class Amount: Codable, CustomStringConvertible { return (leftNormalized.value == rightNormalized.value && leftNormalized.fraction == rightNormalized.fraction) } + /** + Compares two amounts. + + - Parameters: + - left: The amount on the left. + - right: The amount on the right. + + - Throws: + - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. + + - Returns: `true` if and only if `left` is smaller than `right` after normalization, `false` otherwise. + */ static func < (left: Amount, right: Amount) throws -> Bool { if left.currency != right.currency { throw AmountError.incompatibleCurrency @@ -184,10 +380,30 @@ class Amount: Codable, CustomStringConvertible { } } + /** + Compares two amounts. + + - Parameters: + - left: The amount on the left. + - right: The amount on the right. + + - Throws: + - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency. + + - Returns: `true` if and only if `left` is bigger than `right` after normalization, `false` otherwise. + */ static func > (left: Amount, right: Amount) throws -> Bool { return try right < left } + /** + Creates the amount representing zero in a given currency. + + - Parameters: + - currency: The currency to use. + + - Returns: The zero amount for `currency`. + */ static func zero(currency: String) -> Amount { return Amount(currency: currency, value: 0, fraction: 0) } |