taler-ios

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

commit b813f20db60daea2a1d526959f70614f64b1c468
parent 78ef82c82980085344633a0f539da03ab68532e2
Author: Marc Stibane <marc@taler.net>
Date:   Wed,  1 Feb 2023 00:10:44 +0100

enhanced Amount+Time

Diffstat:
Mtaler-swift/Sources/taler-swift/Amount.swift | 161+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mtaler-swift/Sources/taler-swift/Time.swift | 58+++++++++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 152 insertions(+), 67 deletions(-)

diff --git a/taler-swift/Sources/taler-swift/Amount.swift b/taler-swift/Sources/taler-swift/Amount.swift @@ -34,14 +34,14 @@ enum AmountError: Error { } /// A value of some currency. -public class Amount: Codable, CustomStringConvertible { +public class Amount: Codable, Hashable, CustomStringConvertible { /// Format that a currency must match. private static let currencyRegex = #"^[-_*A-Za-z0-9]{1,12}$"# /// The largest possible value that can be represented. private static let maxValue: UInt64 = 1 << 52 - /// The size of `value` in relation to `fraction`. + /// The size of `integer` in relation to `fraction`. private static let fractionalBase: UInt32 = 100000000 /// The greatest number of decimal digits that can be represented. @@ -50,31 +50,38 @@ public class Amount: Codable, CustomStringConvertible { /// 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 integer value of the amount (number to the left of the decimal point). + var integer: 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`". - public var description: String { + + public func hash(into hasher: inout Hasher) { + hasher.combine(currency) + if let normalized = try? normalizedCopy() { + hasher.combine(normalized.integer) + hasher.combine(normalized.fraction) + } else { + hasher.combine(integer) + hasher.combine(fraction) + } + } + + /// The floating point representation of the value + public var value: Double { + let value = Double(integer) if fraction == 0 { - return "\(currency):\(value)" + return value } else { - var frac = fraction - var fracStr = "" - while (frac > 0) { - fracStr += "\(frac / (Amount.fractionalBase / 10))" - frac = (frac * 10) % Amount.fractionalBase - } - return "\(currency):\(value).\(fracStr)" + let thousandths = Double(fraction / (Amount.fractionalBase / 1000)) + return value + (thousandths / 1000.0) } } - - /// The string representation of the amount, formatted as "`value`.`fraction` `currency`". - public var readableDescription: String { + + /// The string representation of the value, formatted as "`integer`.`fraction`". + public var valueStr: String { if fraction == 0 { - return "\(value) \(currency)" + return "\(integer)" } else { var frac = fraction var fracStr = "" @@ -82,24 +89,39 @@ public class Amount: Codable, CustomStringConvertible { fracStr += "\(frac / (Amount.fractionalBase / 10))" frac = (frac * 10) % Amount.fractionalBase } - return "\(value).\(fracStr) \(currency)" + return "\(integer).\(fracStr)" } } + + /// read-only getter + public var currencyStr: String { + return currency + } + + /// The string representation of the amount, formatted as "`currency`:`integer`.`fraction`". + public var description: String { + return "\(currency):\(valueStr)" + } + + /// The string representation of the amount, formatted as "`integer`.`fraction` `currency`". + public var readableDescription: String { + return "\(valueStr) \(currency)" + } /// 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 { if currency.range(of: Amount.currencyRegex, options: .regularExpression) == nil { return false } - return (value <= Amount.maxValue && currency != "") + return (integer <= Amount.maxValue && currency != "") } /// Whether this amount is zero or not. - var isZero: Bool { - return value == 0 && fraction == 0 + public var isZero: Bool { + return integer == 0 && fraction == 0 } - /// Initializes an amount by parsing a string representing the amount. The string should be formatted as "`currency`:`value`.`fraction`". + /// Initializes an amount by parsing a string representing the amount. The string should be formatted as "`currency`:`integer`.`fraction`". /// - Parameters: /// - fromString: The string to parse. /// - Throws: @@ -110,13 +132,13 @@ public class Amount: Codable, CustomStringConvertible { self.currency = String(string[..<separatorIndex]) let amountStr = String(string[string.index(separatorIndex, offsetBy: 1)...]) if let dotIndex = amountStr.firstIndex(of: ".") { - let valueStr = String(amountStr[..<dotIndex]) + let integerStr = String(amountStr[..<dotIndex]) let fractionStr = String(amountStr[string.index(dotIndex, offsetBy: 1)...]) if (fractionStr.count > Amount.fractionalBaseDigits) { throw AmountError.invalidStringRepresentation } - guard let _value = UInt64(valueStr) else { throw AmountError.invalidStringRepresentation } - self.value = _value + guard let intValue = UInt64(integerStr) else { throw AmountError.invalidStringRepresentation } + self.integer = intValue self.fraction = 0 var digitValue = Amount.fractionalBase / 10 for char in fractionStr { @@ -125,26 +147,26 @@ public class Amount: Codable, CustomStringConvertible { digitValue /= 10 } } else { - guard let _value = UInt64(amountStr) else { throw AmountError.invalidStringRepresentation } - self.value = _value + guard let intValue = UInt64(amountStr) else { throw AmountError.invalidStringRepresentation } + self.integer = intValue self.fraction = 0 } } else { self.currency = string - self.value = 0 + self.integer = 0 self.fraction = 0 } guard self.valid else { throw AmountError.invalidAmount } } - /// Initializes an amount with the specified currency, value, and fraction. + /// Initializes an amount with the specified currency, integer, and fraction. /// - Parameters: /// - currency: The currency of the amount. - /// - value: The value of the amount (number to the left of the decimal point). + /// - integer: The integer 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). - public init(currency: String, value: UInt64, fraction: UInt32) { + public init(currency: String, integer: UInt64, fraction: UInt32) { self.currency = currency - self.value = value + self.integer = integer self.fraction = fraction } @@ -163,7 +185,7 @@ public class Amount: Codable, CustomStringConvertible { /// Copies an amount. /// - Returns: A copy of the amount. func copy() -> Amount { - return Amount(currency: self.currency, value: self.value, fraction: self.fraction) + return Amount(currency: currency, integer: integer, fraction: fraction) } /// Creates a normalized copy of an amount (the fractional part is strictly less than one unit of currency). @@ -179,18 +201,18 @@ public class Amount: Codable, CustomStringConvertible { /// - to: The encoder to encode the amount with. public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - try container.encode(self.description) + try container.encode(description) } - /// Normalizes an amount by reducing `fraction` until it is less than `Amount.fractionalBase`, increasing `value` appropriately. + /// Normalizes an amount by reducing `fraction` until it is less than `Amount.fractionalBase`, increasing `integer` appropriately. /// - Throws: /// - `AmountError.invalidAmount` if the amount is invalid either before or after normalization. func normalize() throws { if !valid { throw AmountError.invalidAmount } - self.value += UInt64(self.fraction / Amount.fractionalBase) - self.fraction = self.fraction % Amount.fractionalBase + integer += UInt64(fraction / Amount.fractionalBase) + fraction = fraction % Amount.fractionalBase if !valid { throw AmountError.invalidAmount } @@ -210,7 +232,7 @@ public class Amount: Codable, CustomStringConvertible { let leftNormalized = try left.normalizedCopy() let rightNormalized = try right.normalizedCopy() let result: Amount = leftNormalized - result.value += rightNormalized.value + result.integer += rightNormalized.integer result.fraction += rightNormalized.fraction try result.normalize() return result @@ -230,18 +252,23 @@ public class Amount: Codable, CustomStringConvertible { let leftNormalized = try left.normalizedCopy() let rightNormalized = try right.normalizedCopy() if (leftNormalized.fraction < rightNormalized.fraction) { - guard leftNormalized.value != 0 else { throw AmountError.negativeAmount } - leftNormalized.value -= 1 + guard leftNormalized.integer != 0 else { throw AmountError.negativeAmount } + leftNormalized.integer -= 1 leftNormalized.fraction += Amount.fractionalBase } - guard leftNormalized.value >= rightNormalized.value else { throw AmountError.negativeAmount } + guard leftNormalized.integer >= rightNormalized.integer else { throw AmountError.negativeAmount } let diff = Amount.zero(currency: left.currency) - diff.value = leftNormalized.value - rightNormalized.value + diff.integer = leftNormalized.integer - rightNormalized.integer diff.fraction = leftNormalized.fraction - rightNormalized.fraction try diff.normalize() return diff } - + + public static func diff (_ left: Amount, _ right: Amount) throws -> Amount { + return try left > right ? left - right + : right - left + } + /// Divides an amount by a scalar, possibly introducing rounding error. /// - Parameters: /// - dividend: The amount to divide. @@ -253,8 +280,8 @@ public class Amount: Codable, CustomStringConvertible { if (divisor == 1) { return result } - var remainder = result.value % UInt64(divisor) - result.value = result.value / UInt64(divisor) + var remainder = result.integer % UInt64(divisor) + result.integer = result.integer / UInt64(divisor) remainder = (remainder * UInt64(Amount.fractionalBase)) + UInt64(result.fraction) result.fraction = UInt32(remainder) / divisor try result.normalize() @@ -268,9 +295,9 @@ public class Amount: Codable, CustomStringConvertible { /// - Returns: The product of `amount` and `factor`, normalized. public static func * (amount: Amount, factor: UInt32) throws -> Amount { let result = try amount.normalizedCopy() - result.value = result.value * UInt64(factor) + result.integer = result.integer * UInt64(factor) let fraction_tmp = UInt64(result.fraction) * UInt64(factor) - result.value += fraction_tmp / UInt64(Amount.fractionalBase) + result.integer += fraction_tmp / UInt64(Amount.fractionalBase) result.fraction = UInt32(fraction_tmp % UInt64(Amount.fractionalBase)) return result } @@ -281,16 +308,30 @@ public class Amount: Codable, CustomStringConvertible { /// - 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. - public static func == (left: Amount, right: Amount) throws -> Bool { - if left.currency != right.currency { - throw AmountError.incompatibleCurrency + /// - Returns: `true` if and only if the amounts have the same `integer` and `fraction` after normalization, `false` otherwise. +// public static func == (left: Amount, right: Amount) throws -> Bool { +// if left.currency != right.currency { +// throw AmountError.incompatibleCurrency +// } +// let leftNormalized = try left.normalizedCopy() +// let rightNormalized = try right.normalizedCopy() +// return (leftNormalized.integer == rightNormalized.integer && leftNormalized.fraction == rightNormalized.fraction) +// } + + public static func == (left: Amount, right: Amount) -> Bool { + do { + if left.currency != right.currency { + throw AmountError.incompatibleCurrency + } + let leftNormalized = try left.normalizedCopy() + let rightNormalized = try right.normalizedCopy() + return (leftNormalized.integer == rightNormalized.integer && leftNormalized.fraction == rightNormalized.fraction) + } + catch { + return false } - let leftNormalized = try left.normalizedCopy() - let rightNormalized = try right.normalizedCopy() - return (leftNormalized.value == rightNormalized.value && leftNormalized.fraction == rightNormalized.fraction) } - + /// Compares two amounts. /// - Parameters: /// - left: The amount on the left. @@ -304,10 +345,10 @@ public class Amount: Codable, CustomStringConvertible { } let leftNormalized = try left.normalizedCopy() let rightNormalized = try right.normalizedCopy() - if (leftNormalized.value == rightNormalized.value) { + if (leftNormalized.integer == rightNormalized.integer) { return (leftNormalized.fraction < rightNormalized.fraction) } else { - return (leftNormalized.value < rightNormalized.value) + return (leftNormalized.integer < rightNormalized.integer) } } @@ -349,6 +390,6 @@ public class Amount: Codable, CustomStringConvertible { /// - currency: The currency to use. /// - Returns: The zero amount for `currency`. public static func zero(currency: String) -> Amount { - return Amount(currency: currency, value: 0, fraction: 0) + return Amount(currency: currency, integer: 0, fraction: 0) } } diff --git a/taler-swift/Sources/taler-swift/Time.swift b/taler-swift/Sources/taler-swift/Time.swift @@ -22,21 +22,29 @@ enum TimestampError: Error { /// Invalid arguments were supplied to an arithmetic operation. case invalidArithmeticArguments + + /// The value `never` cannot be returned as UInt64. + case invalidUInt64Value } -/// A point in time, represented by milliseconds from Jaunuary 1, 1970.. -public enum Timestamp: Codable, Equatable { +/// A point in time, represented by milliseconds from January 1, 1970.. +public enum Timestamp: Codable, Hashable { case milliseconds(UInt64) case never enum CodingKeys: String, CodingKey { case t_s = "t_s" + case t_ms = "t_ms" } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) do { - self = Timestamp.milliseconds(try container.decode(UInt64.self, forKey: .t_s) * 1000) + if let seconds: UInt64 = try? container.decode(UInt64.self, forKey: .t_s) { + self = Timestamp.milliseconds(seconds * 1000) + } else { + self = Timestamp.milliseconds(try container.decode(UInt64.self, forKey: .t_ms)) + } } catch { let stringValue = try container.decode(String.self, forKey: .t_s) if stringValue == "never" { @@ -46,11 +54,14 @@ public enum Timestamp: Codable, Equatable { } } } - - static func now() -> Timestamp { - return Timestamp.milliseconds(UInt64(Date().timeIntervalSince1970 * 1000.0)) + public func hash(into hasher: inout Hasher) { + switch self { + case .milliseconds(let t_ms): + hasher.combine(t_ms) + case .never: + hasher.combine("never") + } } - public func encode(to encoder: Encoder) throws { var value = encoder.container(keyedBy: CodingKeys.self) switch self { @@ -62,6 +73,39 @@ public enum Timestamp: Codable, Equatable { } } +extension Timestamp { + /// make a Timestamp for `now` + public static func now() -> Timestamp { + return Timestamp.milliseconds(Date().millisecondsSince1970) + } + + /// convenience initializer from UInt64 (milliseconds from January 1, 1970) + public init(from: UInt64) { + self = Timestamp.milliseconds(from) + } + + /// switch-case the enum here so the caller function doesn't have to + public func milliseconds() throws -> UInt64 { + switch self { + case .milliseconds(let t_ms): + return t_ms + case .never: + throw TimestampError.invalidUInt64Value + } + } +} + +/// extend iOS Date to work with milliseconds since 1970 +extension Date { + public var millisecondsSince1970: UInt64 { + UInt64((timeIntervalSince1970 * 1000.0).rounded()) + } + + public init(milliseconds: UInt64) { + self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000) + } +} + /// A duration of time, measured in milliseconds. public enum Duration: Equatable { case milliseconds(UInt64)