taler-ios

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

commit 28e5f780070ef8f734c9cd989de0dcb34cc63de7
parent adfc9193b9feb82ef37ff0ca8c1dfc1c5b31a9c9
Author: Marc Stibane <marc@taler.net>
Date:   Fri, 20 Oct 2023 08:17:42 +0200

CurrencySpecification

Diffstat:
MTalerWallet.xcodeproj/project.pbxproj | 12++++++------
DTalerWallet1/Helper/CurrencyFormatter.swift | 30------------------------------
ATalerWallet1/Helper/CurrencySpecification.swift | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTalerWallet1/Views/Peer2peer/PaymentPurpose.swift | 8+++++---
MTalerWallet1/Views/Peer2peer/SendPurpose.swift | 8+++++---
Mtaler-swift/Sources/taler-swift/Amount.swift | 37+++----------------------------------
6 files changed, 128 insertions(+), 76 deletions(-)

diff --git a/TalerWallet.xcodeproj/project.pbxproj b/TalerWallet.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 4E16E12329F3BB99008B9C86 /* CurrencyFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16E12229F3BB99008B9C86 /* CurrencyFormatter.swift */; }; + 4E16E12329F3BB99008B9C86 /* CurrencySpecification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16E12229F3BB99008B9C86 /* CurrencySpecification.swift */; }; 4E2254972A822B8100E41D29 /* payment_received.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 4E2254952A822B8100E41D29 /* payment_received.m4a */; }; 4E2254982A822B8100E41D29 /* payment_sent.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 4E2254962A822B8100E41D29 /* payment_sent.m4a */; }; 4E3327BA2AD1635100BF5AD6 /* AsyncSemaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3327B92AD1635100BF5AD6 /* AsyncSemaphore.swift */; }; @@ -74,7 +74,7 @@ 4E3EAE502A990778009F1BE8 /* Model+Transactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095322989CBFE0043A8A1 /* Model+Transactions.swift */; }; 4E3EAE512A990778009F1BE8 /* Controller+playSound.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E578E912A481D8600F21F1C /* Controller+playSound.swift */; }; 4E3EAE522A990778009F1BE8 /* WalletEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095392989CBFE0043A8A1 /* WalletEmptyView.swift */; }; - 4E3EAE532A990778009F1BE8 /* CurrencyFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16E12229F3BB99008B9C86 /* CurrencyFormatter.swift */; }; + 4E3EAE532A990778009F1BE8 /* CurrencySpecification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16E12229F3BB99008B9C86 /* CurrencySpecification.swift */; }; 4E3EAE542A990778009F1BE8 /* TalerDater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095062989CB7C0043A8A1 /* TalerDater.swift */; }; 4E3EAE552A990778009F1BE8 /* Model+Balances.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3B4BC42A428AF700CC88B8 /* Model+Balances.swift */; }; 4E3EAE562A990778009F1BE8 /* LocalizedAlertError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E363CC12A2621C200D7E98C /* LocalizedAlertError.swift */; }; @@ -287,7 +287,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 4E16E12229F3BB99008B9C86 /* CurrencyFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyFormatter.swift; sourceTree = "<group>"; }; + 4E16E12229F3BB99008B9C86 /* CurrencySpecification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencySpecification.swift; sourceTree = "<group>"; }; 4E2254952A822B8100E41D29 /* payment_received.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = payment_received.m4a; sourceTree = "<group>"; }; 4E2254962A822B8100E41D29 /* payment_sent.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = payment_sent.m4a; sourceTree = "<group>"; }; 4E3327B92AD1635100BF5AD6 /* AsyncSemaphore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncSemaphore.swift; sourceTree = "<group>"; }; @@ -569,7 +569,7 @@ 4E363CBD2A23CB2100D7E98C /* AnyTransition+backslide.swift */, 4E3327B92AD1635100BF5AD6 /* AsyncSemaphore.swift */, 4EDBDCD82AB787CB00925C02 /* CallStack.swift */, - 4E16E12229F3BB99008B9C86 /* CurrencyFormatter.swift */, + 4E16E12229F3BB99008B9C86 /* CurrencySpecification.swift */, 4EAD117529F672FA008EDD0B /* KeyboardResponder.swift */, 4E363CC12A2621C200D7E98C /* LocalizedAlertError.swift */, 4E578E912A481D8600F21F1C /* Controller+playSound.swift */, @@ -1084,7 +1084,7 @@ 4E3EAE502A990778009F1BE8 /* Model+Transactions.swift in Sources */, 4E3EAE512A990778009F1BE8 /* Controller+playSound.swift in Sources */, 4E3EAE522A990778009F1BE8 /* WalletEmptyView.swift in Sources */, - 4E3EAE532A990778009F1BE8 /* CurrencyFormatter.swift in Sources */, + 4E3EAE532A990778009F1BE8 /* CurrencySpecification.swift in Sources */, 4E3EAE542A990778009F1BE8 /* TalerDater.swift in Sources */, 4E3EAE552A990778009F1BE8 /* Model+Balances.swift in Sources */, 4E3EAE562A990778009F1BE8 /* LocalizedAlertError.swift in Sources */, @@ -1189,7 +1189,7 @@ 4EB095592989CBFE0043A8A1 /* Model+Transactions.swift in Sources */, 4E578E922A481D8600F21F1C /* Controller+playSound.swift in Sources */, 4EB0955F2989CBFE0043A8A1 /* WalletEmptyView.swift in Sources */, - 4E16E12329F3BB99008B9C86 /* CurrencyFormatter.swift in Sources */, + 4E16E12329F3BB99008B9C86 /* CurrencySpecification.swift in Sources */, 4EB095092989CB7C0043A8A1 /* TalerDater.swift in Sources */, 4E3B4BC52A428AF700CC88B8 /* Model+Balances.swift in Sources */, 4E363CC22A2621C200D7E98C /* LocalizedAlertError.swift in Sources */, diff --git a/TalerWallet1/Helper/CurrencyFormatter.swift b/TalerWallet1/Helper/CurrencyFormatter.swift @@ -1,30 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. - * See LICENSE.md - */ -import Foundation -import taler_swift - -public class CurrencyFormatter: NumberFormatter { - public static let shared = CurrencyFormatter() - - public override init() { - super.init() - self.locale = Locale.current - self.numberStyle = .currency // currencyISOCode, currencyPlural, (currencyAccounting) - self.numberStyle = .currencyISOCode -// self.numberStyle = .spellOut - -// self.currencyCode = code // EUR, USD, JPY, GBP -// self.minimumFractionDigits = fractionDigits -// self.maximumFractionDigits = fractionDigits -// self.groupingSize = 3 // thousands -// self.groupingSeparator = "," -// self.usesGroupingSeparator = true -// self.decimalSeparator = "." - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/TalerWallet1/Helper/CurrencySpecification.swift b/TalerWallet1/Helper/CurrencySpecification.swift @@ -0,0 +1,109 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import Foundation +import taler_swift + +public struct CurrencyInfo { + let scope: ScopeInfo + let specs: CurrencySpecification + let formatter: CurrencyFormatter +} + +public struct CurrencySpecification2: Codable, Sendable { + let currencySpecification: CurrencySpecification +} + +public struct CurrencySpecification: Codable, Sendable { + enum CodingKeys: String, CodingKey { + case name = "name" + case decimalSeparator = "decimal_separator" + case groupSeparator = "group_separator" + case fractionalInputDigits = "num_fractional_input_digits" + case fractionalNormalDigits = "num_fractional_normal_digits" + case fractionalTrailingZeroDigits = "num_fractional_trailing_zero_digits" + case isCurrencyNameLeading = "is_currency_name_leading" + case altUnitNames = "alt_unit_names" + } + /// some name for this CurrencySpecification + let name: String + /// e.g. “.” for $, and “,” for € + let decimalSeparator: String + /// e.g. “,” for $, and “.” or “ ” for € (France uses a narrow space character) + let groupSeparator: String? + /// how much digits the user may enter after the decimal separator + let fractionalInputDigits: Int + /// €,$,£: 2; some arabic currencies: 3, ¥: 0 + let fractionalNormalDigits: Int + /// usually same as numFractionalNormalDigits, but e.g. might be 2 for ¥ + let fractionalTrailingZeroDigits: Int + /// true for “$ 3.50”; false for “3,50 €” + let isCurrencyNameLeading: Bool + /// map of powers of 10 to alternative currency names / symbols + /// must always have an entry under "0" that defines the base name + /// e.g. "0 => €" or "3 => k€". For BTC, would be "0 => BTC, -3 => mBTC". + /// This way, we can also communicate the currency symbol to be used. + let altUnitNames: [Int : String]? +} + + +public class CurrencyFormatter: NumberFormatter { + + var hasAltUnitName0: Bool // specs.altUnitNames[0] should have the Symbol ($,€,¥) + /// factory + static func formatter(scope: ScopeInfo, specs: CurrencySpecification) -> CurrencyFormatter { + let formatter = CurrencyFormatter() + formatter.setCode(to: scope.currency) + formatter.setMinimumFractionDigits(specs.fractionalTrailingZeroDigits) + if let symbol = specs.altUnitNames?[0] { + formatter.setSymbol(to: symbol) + formatter.hasAltUnitName0 = true + } + return formatter + } + + public override init() { + self.hasAltUnitName0 = false + super.init() + self.locale = Locale.current + self.usesGroupingSeparator = true + self.numberStyle = .currencyISOCode // .currency + self.maximumFractionDigits = 8 // ensure that formatter will not round + +// self.currencyCode = code // EUR, USD, JPY, GBP +// self.currencySymbol = symbol +// self.internationalCurrencySymbol = +// self.minimumFractionDigits = fractionDigits +// self.maximumFractionDigits = fractionDigits +// self.groupingSize = 3 // thousands +// self.groupingSeparator = "," +// self.decimalSeparator = "." + } + + func setUseSymbol(_ useSymbol: Bool) { + numberStyle = useSymbol ? .currency : .currencyISOCode + } + + func setCode(to code:String) { + currencyCode = code + } + + func setSymbol(to symbol:String) { + currencySymbol = symbol + } + + func setMinimumFractionDigits(_ digits: Int) { + minimumFractionDigits = digits + } + + func setLocale(to newLocale: String) { + locale = Locale(identifier: newLocale) + maximumFractionDigits = 8 // ensure that formatter will not round +// NumberFormatter.RoundingMode + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/TalerWallet1/Views/Peer2peer/PaymentPurpose.swift b/TalerWallet1/Views/Peer2peer/PaymentPurpose.swift @@ -19,12 +19,14 @@ struct PaymentPurpose: View { @FocusState private var isFocused: Bool - let formatter = CurrencyFormatter.shared // TODO: based on currency +// let formatter = CurrencyFormatter.shared // TODO: based on currency // let buttonFont: Font = .talerTitle2 private var label: String { - let mag = pow(10, formatter.maximumFractionDigits) - return formatter.string(for: Decimal(centsToTransfer) / mag) ?? "" +// let mag = pow(10, formatter.maximumFractionDigits) +// return formatter.string(for: Decimal(centsToTransfer) / mag) ?? "" + + return String(centsToTransfer / 100) } var body: some View { diff --git a/TalerWallet1/Views/Peer2peer/SendPurpose.swift b/TalerWallet1/Views/Peer2peer/SendPurpose.swift @@ -18,11 +18,13 @@ struct SendPurpose: View { @Binding var expireDays: UInt // var deactivateAction: () -> Void - let formatter = CurrencyFormatter.shared // TODO: based on currency +// let formatter = CurrencyFormatter.shared // TODO: based on currency private var value: String { - let mag = pow(10, formatter.maximumFractionDigits) - return formatter.string(for: Decimal(centsToTransfer) / mag) ?? "" +// let mag = pow(10, formatter.maximumFractionDigits) +// return formatter.string(for: Decimal(centsToTransfer) / mag) ?? "" + + return String(centsToTransfer / 100) } var body: some View { diff --git a/taler-swift/Sources/taler-swift/Amount.swift b/taler-swift/Sources/taler-swift/Amount.swift @@ -38,34 +38,6 @@ enum AmountError: Error { case divideByZero } -public struct CurrencySpecification: Codable, Sendable { - enum CodingKeys: String, CodingKey { - case decimalSeparator = "decimal_separator" - case name = "name" - case fractionalInputDigits = "num_fractional_input_digits" - case fractionalNormalDigits = "num_fractional_normal_digits" - case fractionalTrailingZeroDigits = "num_fractional_trailing_zero_digits" - case isCurrencyNameLeading = "is_currency_name_leading" - case altUnitNames = "alt_unit_names" - } - /// e.g. “.” for $ and ¥; “,” for € - let decimalSeparator: String - /// some name for this CurrencySpecification - let name: String - /// how much digits the user may enter after the decimal separator - let fractionalInputDigits: Int - /// €,$,£: 2; some arabic currencies: 3, ¥: 0 - let fractionalNormalDigits: Int - /// usually same as numFractionalNormalDigits, but e.g. might be 2 for ¥ - let fractionalTrailingZeroDigits: Int - /// true for “$ 3.50”; false for “3,50 €” - let isCurrencyNameLeading: Bool - /// map of powers of 10 to alternative currency names / symbols - /// must always have an entry under "0" that defines the base name - /// e.g. "0 => €" or "3 => k€". For BTC, would be "0 => BTC, -3 => mBTC". - /// This way, we can also communicate the currency symbol to be used. - let altUnitNames: [Int : String] -} /// A value of some currency. public final class Amount: Codable, Hashable, @unchecked Sendable, CustomStringConvertible { // TODO: @unchecked @@ -90,9 +62,6 @@ public final class Amount: Codable, Hashable, @unchecked Sendable, CustomStringC /// The fractional value of the amount (number to the right of the decimal point). var fraction: UInt32 - /// Additional info for formatting currency strings - var currencySpecification: CurrencySpecification? - public func hash(into hasher: inout Hasher) { hasher.combine(currency) if let normalized = try? normalizedCopy() { @@ -118,9 +87,9 @@ public final class Amount: Codable, Hashable, @unchecked Sendable, CustomStringC /// The string representation of the value, formatted as "`integer`.`fraction`". public var valueStr: String { var decimalSeparator = "." - if let currencySpecification { // TODO: use locale - decimalSeparator = currencySpecification.decimalSeparator - } +// if let currencySpecification { // TODO: use locale +// decimalSeparator = currencySpecification.decimalSeparator +// } if fraction == 0 { return "\(integer)" } else {