/* * This file is part of GNU Taler * (C) 2021 Taler Systems S.A. * * GNU Taler is free software; you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation; either version 3, or (at your option) any later version. * * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * GNU Taler; see the file COPYING. If not, see */ import Foundation enum AmountError: Error { case invalidStringRepresentation case incompatibleCurrency case invalidAmount case negativeAmount } class Amount: Codable, CustomStringConvertible { private static let maxValue: UInt64 = 1 << 52 private static let fractionalBase: UInt32 = 100000000 private static let fractionalBaseDigits: UInt = 8 var currency: String var value: UInt64 var fraction: UInt32 var description: String { if fraction == 0 { return "\(currency):\(value)" } else { var frac = fraction var fracStr = "" while (frac > 0) { fracStr += "\(frac / (Amount.fractionalBase / 10))" frac = (frac * 10) % Amount.fractionalBase } return "\(currency):\(value).\(fracStr)" } } var valid: Bool { return (value <= Amount.maxValue && currency != "") } init(fromString string: String) throws { guard let separatorIndex = string.firstIndex(of: ":") else { throw AmountError.invalidStringRepresentation } self.currency = String(string[.. Amount { return Amount(currency: self.currency, value: self.value, fraction: self.fraction) } func normalizedCopy() throws -> Amount { let amount = self.copy() try amount.normalize() return amount } func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(self.description) } func normalize() throws { if !valid { throw AmountError.invalidAmount } self.value += UInt64(self.fraction / Amount.fractionalBase) self.fraction = self.fraction % Amount.fractionalBase if !valid { throw AmountError.invalidAmount } } static func + (left: Amount, right: Amount) throws -> Amount { if left.currency != right.currency { throw AmountError.incompatibleCurrency } let leftNormalized = try left.normalizedCopy() let rightNormalized = try right.normalizedCopy() let result: Amount = leftNormalized result.value += rightNormalized.value result.fraction += rightNormalized.fraction try result.normalize() return result } static func - (left: Amount, right: Amount) throws -> Amount { if left.currency != right.currency { throw AmountError.incompatibleCurrency } 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 leftNormalized.fraction += Amount.fractionalBase } guard leftNormalized.value >= rightNormalized.value else { throw AmountError.negativeAmount } let diff = Amount.zero(currency: left.currency) diff.value = leftNormalized.value - rightNormalized.value diff.fraction = leftNormalized.fraction - rightNormalized.fraction try diff.normalize() return diff } 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.value == rightNormalized.value && leftNormalized.fraction == rightNormalized.fraction) } 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() if (leftNormalized.value == rightNormalized.value) { return (leftNormalized.fraction < rightNormalized.fraction) } else { return (leftNormalized.value < rightNormalized.value) } } static func > (left: Amount, right: Amount) throws -> Bool { return try right < left } static func zero(currency: String) -> Amount { return Amount(currency: currency, value: 0, fraction: 0) } }