summaryrefslogtreecommitdiff
path: root/Taler/Amount.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Taler/Amount.swift')
-rw-r--r--Taler/Amount.swift194
1 files changed, 194 insertions, 0 deletions
diff --git a/Taler/Amount.swift b/Taler/Amount.swift
new file mode 100644
index 0000000..83445b4
--- /dev/null
+++ b/Taler/Amount.swift
@@ -0,0 +1,194 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+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[..<separatorIndex])
+ let amountStr = String(string[string.index(separatorIndex, offsetBy: 1)...])
+ if let dotIndex = amountStr.firstIndex(of: ".") {
+ let valueStr = String(amountStr[..<dotIndex])
+ let fractionStr = String(amountStr[string.index(dotIndex, offsetBy: 1)...])
+ guard let _value = UInt64(valueStr) else { throw AmountError.invalidStringRepresentation }
+ self.value = _value
+ self.fraction = 0
+ var digitValue = Amount.fractionalBase / 10
+ for char in fractionStr {
+ guard let digit = char.wholeNumberValue else { throw AmountError.invalidStringRepresentation }
+ self.fraction += digitValue * UInt32(digit)
+ digitValue /= 10
+ }
+ } else {
+ guard let _value = UInt64(amountStr) else { throw AmountError.invalidStringRepresentation }
+ self.value = _value
+ self.fraction = 0
+ }
+ guard self.valid else { throw AmountError.invalidAmount }
+ }
+
+ init(currency: String, value: UInt64, fraction: UInt32) {
+ self.currency = currency
+ self.value = value
+ self.fraction = fraction
+ }
+
+ init(fromDecoder decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ let string = try container.decode(String.self)
+ /* TODO: de-duplicate */
+ guard let separatorIndex = string.firstIndex(of: ":") else { throw AmountError.invalidStringRepresentation }
+ 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 fractionStr = String(amountStr[string.index(dotIndex, offsetBy: 1)...])
+ guard let _value = UInt64(valueStr) else { throw AmountError.invalidStringRepresentation }
+ self.value = _value
+ self.fraction = 0
+ var digitValue = Amount.fractionalBase / 10
+ for char in fractionStr {
+ guard let digit = char.wholeNumberValue else { throw AmountError.invalidStringRepresentation }
+ self.fraction += digitValue * UInt32(digit)
+ digitValue /= 10
+ }
+ } else {
+ guard let _value = UInt64(amountStr) else { throw AmountError.invalidStringRepresentation }
+ self.value = _value
+ self.fraction = 0
+ }
+ guard self.valid else { throw AmountError.invalidAmount }
+ }
+
+ func copy() -> 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)
+ }
+}