Update formatting

This commit is contained in:
Ali 2021-04-02 21:19:54 +04:00
parent 20325dd69c
commit a6e2f05b3a
10 changed files with 1696 additions and 15 deletions

View File

@ -44,7 +44,7 @@ private enum BotCheckoutSection: Int32 {
enum BotCheckoutEntry: ItemListNodeEntry {
case header(PresentationTheme, TelegramMediaInvoice, String)
case price(Int, PresentationTheme, String, String, Bool, Bool)
case tip(Int, PresentationTheme, String, String, String, Int64, [(String, Int64)])
case tip(Int, PresentationTheme, String, String, String, Int64, Int64, [(String, Int64)])
case paymentMethod(PresentationTheme, String, String)
case shippingInfo(PresentationTheme, String, String)
case shippingMethod(PresentationTheme, String, String)
@ -69,7 +69,7 @@ enum BotCheckoutEntry: ItemListNodeEntry {
return 0
case let .price(index, _, _, _, _, _):
return 1 + Int32(index)
case let .tip(index, _, _, _, _, _, _):
case let .tip(index, _, _, _, _, _, _, _):
return 1 + Int32(index)
case .paymentMethod:
return 10000 + 2
@ -127,8 +127,8 @@ enum BotCheckoutEntry: ItemListNodeEntry {
} else {
return false
}
case let .tip(lhsIndex, lhsTheme, lhsText, lhsCurrency, lhsValue, lhsNumericValue, lhsVariants):
if case let .tip(rhsIndex, rhsTheme, rhsText, rhsCurrency, rhsValue, rhsNumericValue, rhsVariants) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsText == rhsText, lhsCurrency == rhsCurrency, lhsValue == rhsValue, lhsNumericValue == rhsNumericValue {
case let .tip(lhsIndex, lhsTheme, lhsText, lhsCurrency, lhsValue, lhsNumericValue, lhsMaxValue, lhsVariants):
if case let .tip(rhsIndex, rhsTheme, rhsText, rhsCurrency, rhsValue, rhsNumericValue, rhsMaxValue, rhsVariants) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsText == rhsText, lhsCurrency == rhsCurrency, lhsValue == rhsValue, lhsNumericValue == rhsNumericValue, lhsMaxValue == rhsMaxValue {
if lhsVariants.count != rhsVariants.count {
return false
}
@ -194,8 +194,8 @@ enum BotCheckoutEntry: ItemListNodeEntry {
return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section)
case let .price(_, theme, text, value, isFinal, hasSeparator):
return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: hasSeparator, sectionId: self.section)
case let .tip(_, _, text, currency, value, numericValue, variants):
return BotCheckoutTipItem(theme: presentationData.theme, title: text, currency: currency, value: value, numericValue: numericValue, availableVariants: variants, sectionId: self.section, updateValue: { value in
case let .tip(_, _, text, currency, value, numericValue, maxValue, variants):
return BotCheckoutTipItem(theme: presentationData.theme, strings: presentationData.strings, title: text, currency: currency, value: value, numericValue: numericValue, maxValue: maxValue, availableVariants: variants, sectionId: self.section, updateValue: { value in
arguments.updateTip(value)
})
case let .paymentMethod(_, text, value):
@ -324,7 +324,7 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
let tipTitle: String
//TODO:localize
tipTitle = "Tip (Optional)"
entries.append(.tip(index, presentationData.theme, tipTitle, paymentForm.invoice.currency, "\(formatCurrencyAmount(currentTip ?? 0, currency: paymentForm.invoice.currency))", currentTip ?? 0, tip.suggested.map { item -> (String, Int64) in
entries.append(.tip(index, presentationData.theme, tipTitle, paymentForm.invoice.currency, "\(formatCurrencyAmount(currentTip ?? 0, currency: paymentForm.invoice.currency))", currentTip ?? 0, tip.max, tip.suggested.map { item -> (String, Int64) in
return ("\(formatCurrencyAmount(item, currency: paymentForm.invoice.currency))", item)
}))
index += 1

View File

@ -10,10 +10,12 @@ import TelegramStringFormatting
class BotCheckoutTipItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let title: String
let currency: String
let value: String
let numericValue: Int64
let maxValue: Int64
let availableVariants: [(String, Int64)]
let updateValue: (Int64) -> Void
@ -21,12 +23,14 @@ class BotCheckoutTipItem: ListViewItem, ItemListItem {
let requestsNoInset: Bool = true
init(theme: PresentationTheme, title: String, currency: String, value: String, numericValue: Int64, availableVariants: [(String, Int64)], sectionId: ItemListSectionId, updateValue: @escaping (Int64) -> Void) {
init(theme: PresentationTheme, strings: PresentationStrings, title: String, currency: String, value: String, numericValue: Int64, maxValue: Int64, availableVariants: [(String, Int64)], sectionId: ItemListSectionId, updateValue: @escaping (Int64) -> Void) {
self.theme = theme
self.strings = strings
self.title = title
self.currency = currency
self.value = value
self.numericValue = numericValue
self.maxValue = maxValue
self.availableVariants = availableVariants
self.updateValue = updateValue
self.sectionId = sectionId
@ -154,6 +158,8 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
let labelNode: TextNode
private let textNode: TextFieldNode
private var formatterDelegate: CurrencyUITextFieldDelegate?
private let scrollNode: ASScrollNode
private var valueNodes: [TipValueNode] = []
@ -187,7 +193,6 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
self.addSubnode(self.scrollNode)
self.textNode.clipsToBounds = true
self.textNode.textField.delegate = self
self.textNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged)
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
}
@ -216,6 +221,7 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
//TODO:locali
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Enter Custom", font: textFont, textColor: textColor.withMultipliedAlpha(0.8)), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
@ -241,6 +247,25 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
strongSelf.labelNode.isHidden = !text.isEmpty
}
if strongSelf.formatterDelegate == nil {
strongSelf.formatterDelegate = CurrencyUITextFieldDelegate(formatter: CurrencyFormatter(currency: item.currency, { formatter in
formatter.maxValue = currencyToFractionalAmount(value: item.maxValue, currency: item.currency) ?? 10000.0
formatter.minValue = 0.0
formatter.hasDecimals = true
}))
strongSelf.formatterDelegate?.passthroughDelegate = strongSelf
strongSelf.formatterDelegate?.textUpdated = {
guard let strongSelf = self else {
return
}
strongSelf.textFieldTextChanged(strongSelf.textNode.textField)
}
strongSelf.textNode.clipsToBounds = true
strongSelf.textNode.textField.delegate = strongSelf.formatterDelegate
}
strongSelf.textNode.textField.typingAttributes = [NSAttributedString.Key.font: titleFont]
strongSelf.textNode.textField.font = titleFont
@ -323,11 +348,6 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
}
@objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let item = self.item else {
return false
}
let newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
return true
}

View File

@ -0,0 +1,178 @@
//
// CurrencyCode.swift
// CurrencyText
//
// Created by Felipe Lefèvre Marino on 1/26/19.
//
import Foundation
/// Currency wraps all availabe currencies that can represented as formatted monetary values
/// A currency code is a three-letter code that is, in most cases,
/// composed of a countrys two-character Internet country code plus an extra character
/// to denote the currency unit. For example, the currency code for the Australian
/// dollar is AUD. Currency codes are based on the ISO 4217 standard
public enum Currency: String {
case afghani = "AFN",
algerianDinar = "DZD",
argentinePeso = "ARS",
armenianDram = "AMD",
arubanFlorin = "AWG",
australianDollar = "AUD",
azerbaijanManat = "AZN",
bahamianDollar = "BSD",
bahrainiDinar = "BHD",
baht = "THB",
balboa = "PAB",
barbadosDollar = "BBD",
belarusianRuble = "BYN",
belizeDollar = "BZD",
bermudianDollar = "BMD",
boliviano = "BOB",
bolívar = "VEF",
brazilianReal = "BRL",
bruneiDollar = "BND",
bulgarianLev = "BGN",
burundiFranc = "BIF",
caboVerdeEscudo = "CVE",
canadianDollar = "CAD",
caymanIslandsDollar = "KYD",
chileanPeso = "CLP",
colombianPeso = "COP",
comorianFranc = "KMF",
congoleseFranc = "CDF",
convertibleMark = "BAM",
cordobaOro = "NIO",
costaRicanColon = "CRC",
cubanPeso = "CUP",
czechKoruna = "CZK",
dalasi = "GMD",
danishKrone = "DKK",
denar = "MKD",
djiboutiFranc = "DJF",
dobra = "STN",
dollar = "USD",
dominicanPeso = "DOP",
dong = "VND",
eastCaribbeanDollar = "XCD",
egyptianPound = "EGP",
elSalvadorColon = "SVC",
ethiopianBirr = "ETB",
euro = "EUR",
falklandIslandsPound = "FKP",
fijiDollar = "FJD",
forint = "HUF",
ghanaCedi = "GHS",
gibraltarPound = "GIP",
gourde = "HTG",
guarani = "PYG",
guineanFranc = "GNF",
guyanaDollar = "GYD",
hongKongDollar = "HKD",
hryvnia = "UAH",
icelandKrona = "ISK",
indianRupee = "INR",
iranianRial = "IRR",
iraqiDinar = "IQD",
jamaicanDollar = "JMD",
jordanianDinar = "JOD",
kenyanShilling = "KES",
kina = "PGK",
kuna = "HRK",
kuwaitiDinar = "KWD",
kwanza = "AOA",
kyat = "MMK",
laoKip = "LAK",
lari = "GEL",
lebanesePound = "LBP",
lek = "ALL",
lempira = "HNL",
leone = "SLL",
liberianDollar = "LRD",
libyanDinar = "LYD",
lilangeni = "SZL",
loti = "LSL",
malagasyAriary = "MGA",
malawiKwacha = "MWK",
malaysianRinggit = "MYR",
mauritiusRupee = "MUR",
mexicanPeso = "MXN",
mexicanUnidadDeInversion = "MXV",
moldovanLeu = "MDL",
moroccanDirham = "MAD",
mozambiqueMetical = "MZN",
mvdol = "BOV",
naira = "NGN",
nakfa = "ERN",
namibiaDollar = "NAD",
nepaleseRupee = "NPR",
netherlandsAntilleanGuilder = "ANG",
newIsraeliSheqel = "ILS",
newTaiwanDollar = "TWD",
newZealandDollar = "NZD",
ngultrum = "BTN",
northKoreanWon = "KPW",
norwegianKrone = "NOK",
ouguiya = "MRU",
paanga = "TOP",
pakistanRupee = "PKR",
pataca = "MOP",
pesoConvertible = "CUC",
pesoUruguayo = "UYU",
philippinePiso = "PHP",
poundSterling = "GBP",
pula = "BWP",
qatariRial = "QAR",
quetzal = "GTQ",
rand = "ZAR",
rialOmani = "OMR",
riel = "KHR",
romanianLeu = "RON",
rufiyaa = "MVR",
rupiah = "IDR",
russianRuble = "RUB",
rwandaFranc = "RWF",
saintHelenaPound = "SHP",
saudiRiyal = "SAR",
serbianDinar = "RSD",
seychellesRupee = "SCR",
singaporeDollar = "SGD",
sol = "PEN",
solomonIslandsDollar = "SBD",
som = "KGS",
somaliShilling = "SOS",
somoni = "TJS",
southSudanesePound = "SSP",
sriLankaRupee = "LKR",
sudanesePound = "SDG",
surinamDollar = "SRD",
swedishKrona = "SEK",
swissFranc = "CHF",
syrianPound = "SYP",
taka = "BDT",
tala = "WST",
tanzanianShilling = "TZS",
tenge = "KZT",
trinidadAndTobagoDollar = "TTD",
tugrik = "MNT",
tunisianDinar = "TND",
turkishLira = "TRY",
turkmenistanNewManat = "TMT",
uaeDirham = "AED",
ugandaShilling = "UGX",
unidadDeFomento = "CLF",
unidadDeValorReal = "COU",
uruguayPesoEnUnidadesIndexadas = "UYI",
uzbekistanSum = "UZS",
vatu = "VUV",
wirEuro = "CHE",
wirFranc = "CHW",
won = "KRW",
yemeniRial = "YER",
yen = "JPY",
yuanRenminbi = "CNY",
zambianKwacha = "ZMW",
zimbabweDollar = "ZWL",
zloty = "PLN",
none
}

View File

@ -0,0 +1,345 @@
//
// CurrencyFormatter.swift
// CurrencyText
//
// Created by Felipe Lefèvre Marino on 1/27/19.
//
import Foundation
import TelegramStringFormatting
// MARK: - Currency protocols
public protocol CurrencyFormatting {
var maxDigitsCount: Int { get }
var decimalDigits: Int { get set }
var maxValue: Double? { get set }
var minValue: Double? { get set }
var initialText: String { get }
var currencySymbol: String { get set }
func string(from double: Double) -> String?
func unformatted(string: String) -> String?
func double(from string: String) -> Double?
}
public protocol CurrencyAdjusting {
func formattedStringWithAdjustedDecimalSeparator(from string: String) -> String?
func formattedStringAdjustedToFitAllowedValues(from string: String) -> String?
}
// MARK: - Currency formatter
public class CurrencyFormatter: CurrencyFormatting {
/// Set the locale to retrieve the currency from
/// You can pass a Swift type Locale or one of the
/// Locales enum options - that encapsulates all available locales.
public var locale: LocaleConvertible {
set { self.numberFormatter.locale = newValue.locale }
get { self.numberFormatter.locale }
}
/// Set the desired currency type
/// * Note: The currency take effetcs above the displayed currency symbol,
/// however details such as decimal separators, grouping separators and others
/// will be set based on the defined locale. So for a precise experience, please
/// preferarbly setup both, when you are setting a currency that does not match the
/// default/current user locale.
public var currency: Currency {
set { numberFormatter.currencyCode = newValue.rawValue }
get { Currency(rawValue: numberFormatter.currencyCode) ?? .dollar }
}
/// Define if currency symbol should be presented or not.
/// Note: when set to false the current currency symbol is removed
public var showCurrencySymbol: Bool = true {
didSet {
numberFormatter.currencySymbol = showCurrencySymbol ? numberFormatter.currencySymbol : ""
}
}
/// The currency's symbol.
/// Can be used to read or set a custom symbol.
/// Note: showCurrencySymbol must be set to true for
/// the currencySymbol to be correctly changed.
public var currencySymbol: String {
set {
guard showCurrencySymbol else { return }
numberFormatter.currencySymbol = newValue
}
get { numberFormatter.currencySymbol }
}
/// The lowest number allowed as input.
/// This value is initially set to the text field text
/// when defined.
public var minValue: Double? {
set {
guard let newValue = newValue else { return }
numberFormatter.minimum = NSNumber(value: newValue)
}
get {
if let minValue = numberFormatter.minimum {
return Double(truncating: minValue)
}
return nil
}
}
/// The highest number allowed as input.
/// The text field will not allow the user to increase the input
/// value beyond it, when defined.
public var maxValue: Double? {
set {
guard let newValue = newValue else { return }
numberFormatter.maximum = NSNumber(value: newValue)
}
get {
if let maxValue = numberFormatter.maximum {
return Double(truncating: maxValue)
}
return nil
}
}
/// The number of decimal digits shown.
/// default is set to zero.
/// * Example: With decimal digits set to 3, if the value to represent is "1",
/// the formatted text in the fractions will be ",001".
/// Other than that with the value as 1, the formatted text fractions will be ",1".
public var decimalDigits: Int {
set {
numberFormatter.minimumFractionDigits = newValue
numberFormatter.maximumFractionDigits = newValue
}
get { numberFormatter.minimumFractionDigits }
}
/// Set decimal numbers behavior.
/// When set to true decimalDigits are automatically set to 2 (most currencies pattern),
/// and the decimal separator is presented. Otherwise decimal digits are not shown and
/// the separator gets hidden as well
/// When reading it returns the current pattern based on the setup.
/// Note: Setting decimal digits after, or alwaysShowsDecimalSeparator can overlap this definitios,
/// and should be only done if you need specific cases
public var hasDecimals: Bool {
set {
self.decimalDigits = newValue ? 2 : 0
self.numberFormatter.alwaysShowsDecimalSeparator = newValue ? true : false
}
get { decimalDigits != 0 }
}
/// Defines the string that is the decimal separator
/// Note: only presented when hasDecimals is true OR decimalDigits
/// is greater than 0.
public var decimalSeparator: String {
set { self.numberFormatter.currencyDecimalSeparator = newValue }
get { numberFormatter.currencyDecimalSeparator }
}
/// Can be used to set a custom currency code string
public var currencyCode: String {
set { self.numberFormatter.currencyCode = newValue }
get { numberFormatter.currencyCode }
}
/// Sets if decimal separator should always be presented,
/// even when decimal digits are disabled
public var alwaysShowsDecimalSeparator: Bool {
set { self.numberFormatter.alwaysShowsDecimalSeparator = newValue }
get { numberFormatter.alwaysShowsDecimalSeparator }
}
/// The amount of grouped numbers. This definition is fixed for at least
/// the first non-decimal group of numbers, and is applied to all other
/// groups if secondaryGroupingSize does not have another value.
public var groupingSize: Int {
set { self.numberFormatter.groupingSize = newValue }
get { numberFormatter.groupingSize }
}
/// The amount of grouped numbers after the first group.
/// Example: for the given value of 99999999999, when grouping size
/// is set to 3 and secondaryGroupingSize has 4 as value,
/// the number is represented as: (9999) (9999) [999].
/// Beign [] grouping size and () secondary grouping size.
public var secondaryGroupingSize: Int {
set { self.numberFormatter.secondaryGroupingSize = newValue }
get { numberFormatter.secondaryGroupingSize }
}
/// Defines the string that is shown between groups of numbers
/// * Example: a monetary value of a thousand (1000) with a grouping
/// separator == "." is represented as `1.000` *.
/// Note: It automatically sets hasGroupingSeparator to true.
public var groupingSeparator: String {
set {
self.numberFormatter.currencyGroupingSeparator = newValue
self.numberFormatter.usesGroupingSeparator = true
}
get { self.numberFormatter.currencyGroupingSeparator }
}
/// Sets if has separator between all group of numbers.
/// * Example: when set to false, a bug number such as a million
/// is represented by tight numbers "1000000". Otherwise if set
/// to true each group is separated by the defined `groupingSeparator`. *
/// Note: When set to true only works by defining a grouping separator.
public var hasGroupingSeparator: Bool {
set { self.numberFormatter.usesGroupingSeparator = newValue }
get { self.numberFormatter.usesGroupingSeparator }
}
/// Value that will be presented when the text field
/// text values matches zero (0)
public var zeroSymbol: String? {
set { numberFormatter.zeroSymbol = newValue }
get { numberFormatter.zeroSymbol }
}
/// Value that will be presented when the text field
/// is empty. The default is "" - empty string
public var nilSymbol: String {
set { numberFormatter.nilSymbol = newValue }
get { return numberFormatter.nilSymbol }
}
/// Encapsulated Number formatter
let numberFormatter: NumberFormatter
/// Maximum allowed number of integers
public var maxIntegers: Int? {
set {
guard let maxIntegers = newValue else { return }
numberFormatter.maximumIntegerDigits = maxIntegers
}
get { return numberFormatter.maximumIntegerDigits }
}
/// Returns the maximum allowed number of numerical characters
public var maxDigitsCount: Int {
numberFormatter.maximumIntegerDigits + numberFormatter.maximumFractionDigits
}
/// The value zero formatted to serve as initial text.
public var initialText: String {
numberFormatter.string(from: 0) ?? "0.0"
}
//MARK: - INIT
/// Handler to initialize a new style.
public typealias InitHandler = ((CurrencyFormatter) -> (Void))
/// Initialize a new currency formatter with optional configuration handler callback.
///
/// - Parameter handler: configuration handler callback.
public init(currency: String, _ handler: InitHandler? = nil) {
numberFormatter = setupCurrencyNumberFormatter(currency: currency)
numberFormatter.alwaysShowsDecimalSeparator = false
/*numberFormatter.numberStyle = .currency
numberFormatter.minimumFractionDigits = 2
numberFormatter.maximumFractionDigits = 2
numberFormatter.minimumIntegerDigits = 1*/
handler?(self)
}
}
// MARK: Format
extension CurrencyFormatter {
/// Returns a currency string from a given double value.
///
/// - Parameter double: the monetary amount.
/// - Returns: formatted currency string.
public func string(from double: Double) -> String? {
let validValue = valueAdjustedToFitAllowedValues(from: double)
return numberFormatter.string(from: validValue)
}
/// Returns a double from a string that represents a numerical value.
///
/// - Parameter string: string that describes the numerical value.
/// - Returns: the value as a Double.
public func double(from string: String) -> Double? {
Double(string)
}
/// Receives a currency formatted string and returns its
/// numerical/unformatted representation.
///
/// - Parameter string: currency formatted string
/// - Returns: numerical representation
public func unformatted(string: String) -> String? {
string.numeralFormat()
}
}
// MARK: - Currency adjusting conformance
extension CurrencyFormatter: CurrencyAdjusting {
/// Receives a currency formatted String, and returns it with its decimal separator adjusted.
///
/// _Note_: Useful when appending values to a currency formatted String.
/// E.g. "$ 23.24" after users taps an additional number, is equal = "$ 23.247".
/// Which gets updated to "$ 232.47".
///
/// - Parameter string: The currency formatted String
/// - Returns: The currency formatted received String with its decimal separator adjusted
public func formattedStringWithAdjustedDecimalSeparator(from string: String) -> String? {
let adjustedString = numeralStringWithAdjustedDecimalSeparator(from: string)
guard let value = double(from: adjustedString) else { return nil }
return self.numberFormatter.string(from: value)
}
/// Receives a currency formatted String, and returns it to fit the formatter's min and max values, when needed.
///
/// - Parameter string: The currency formatted String
/// - Returns: The currency formatted String, or the formatted version of its closes allowed value, min or max, depending on the closest boundary.
public func formattedStringAdjustedToFitAllowedValues(from string: String) -> String? {
let adjustedString = numeralStringWithAdjustedDecimalSeparator(from: string)
guard let originalValue = double(from: adjustedString) else { return nil }
return self.string(from: originalValue)
}
/// Receives a currency formatted String, and returns a numeral version of it with its decimal separator adjusted.
///
/// E.g. "$ 23.24", after users taps an additional number, get equal as "$ 23.247". The returned value would be "232.47".
///
/// - Parameter string: The currency formatted String
/// - Returns: The received String with numeral format and with its decimal separator adjusted
private func numeralStringWithAdjustedDecimalSeparator(from string: String) -> String {
var updatedString = string.numeralFormat()
let isNegative: Bool = string.contains(String.negativeSymbol)
updatedString = isNegative ? .negativeSymbol + updatedString : updatedString
updatedString.updateDecimalSeparator(decimalDigits: decimalDigits)
return updatedString
}
/// Receives a Double value, and returns it adjusted to fit min and max allowed values, when needed.
/// If the value respect number formatter's min and max, it will be returned without changes.
///
/// - Parameter value: The value to be adjusted if needed
/// - Returns: The value updated or not, depending on the formatter's settings
private func valueAdjustedToFitAllowedValues(from value: Double) -> Double {
if let minValue = minValue, value < minValue {
return minValue
} else if let maxValue = maxValue, value > maxValue {
return maxValue
}
return value
}
}

View File

@ -0,0 +1,755 @@
//
// CurrencyLocale.swift
// CurrencyText
//
// Created by Felipe Lefèvre Marino on 1/26/19.
//
import Foundation
/// All locales were extracted from:
/// jacobbubu/ioslocaleidentifiers.csv - https://gist.github.com/jacobbubu/1836273
/// The LocaleConvertible pattern is inspired in SwiftDate by malcommac
/// https://github.com/malcommac/SwiftDate
/// LocaleConvertible defines the behavior to convert locale info to system Locale type
public protocol LocaleConvertible {
var locale: Locale { get }
}
extension Locale: LocaleConvertible {
public var locale: Locale { return self }
}
/// Defines locales available in system
public enum CurrencyLocale: String, LocaleConvertible {
case current = "current"
case autoUpdating = "currentAutoUpdating"
case afrikaans = "af"
case afrikaansNamibia = "af_NA"
case afrikaansSouthAfrica = "af_ZA"
case aghem = "agq"
case aghemCameroon = "agq_CM"
case akan = "ak"
case akanGhana = "ak_GH"
case albanian = "sq"
case albanianAlbania = "sq_AL"
case albanianKosovo = "sq_XK"
case albanianMacedonia = "sq_MK"
case amharic = "am"
case amharicEthiopia = "am_ET"
case arabic = "ar"
case arabicAlgeria = "ar_DZ"
case arabicBahrain = "ar_BH"
case arabicChad = "ar_TD"
case arabicComoros = "ar_KM"
case arabicDjibouti = "ar_DJ"
case arabicEgypt = "ar_EG"
case arabicEritrea = "ar_ER"
case arabicIraq = "ar_IQ"
case arabicIsrael = "ar_IL"
case arabicJordan = "ar_JO"
case arabicKuwait = "ar_KW"
case arabicLebanon = "ar_LB"
case arabicLibya = "ar_LY"
case arabicMauritania = "ar_MR"
case arabicMorocco = "ar_MA"
case arabicOman = "ar_OM"
case arabicPalestinianTerritories = "ar_PS"
case arabicQatar = "ar_QA"
case arabicSaudiArabia = "ar_SA"
case arabicSomalia = "ar_SO"
case arabicSouthSudan = "ar_SS"
case arabicSudan = "ar_SD"
case arabicSyria = "ar_SY"
case arabicTunisia = "ar_TN"
case arabicUnitedArabEmirates = "ar_AE"
case arabicWesternSahara = "ar_EH"
case arabicWorld = "ar_001"
case arabicYemen = "ar_YE"
case armenian = "hy"
case armenianArmenia = "hy_AM"
case assamese = "as"
case assameseIndia = "as_IN"
case asu = "asa"
case asuTanzania = "asa_TZ"
case azerbaijani = "az_Latn"
case azerbaijaniAzerbaijan = "az_Latn_AZ"
case azerbaijaniCyrillic = "az_Cyrl"
case azerbaijaniCyrillicAzerbaijan = "az_Cyrl_AZ"
case bafia = "ksf"
case bafiaCameroon = "ksf_CM"
case bambara = "bm_Latn"
case bambaraMali = "bm_Latn_ML"
case basaa = "bas"
case basaaCameroon = "bas_CM"
case basque = "eu"
case basqueSpain = "eu_ES"
case belarusian = "be"
case belarusianBelarus = "be_BY"
case bemba = "bem"
case bembaZambia = "bem_ZM"
case bena = "bez"
case benaTanzania = "bez_TZ"
case bengali = "bn"
case bengaliBangladesh = "bn_BD"
case engaliIndia = "bn_IN"
case bodo = "brx"
case bodoIndia = "brx_IN"
case bosnian = "bs_Latn"
case bosnianBosniaHerzegovina = "bs_Latn_BA"
case bosnianCyrillic = "bs_Cyrl"
case bosnianCyrillicBosniaHerzegovina = "bs_Cyrl_BA"
case breton = "br"
case bretonFrance = "br_FR"
case bulgarian = "bg"
case bulgarianBulgaria = "bg_BG"
case burmese = "my"
case burmeseMyanmarBurma = "my_MM"
case catalan = "ca"
case catalanAndorra = "ca_AD"
case catalanFrance = "ca_FR"
case catalanItaly = "ca_IT"
case catalanSpain = "ca_ES"
case centralAtlasTamazight = "tzm_Latn"
case centralAtlasTamazightMorocco = "tzm_Latn_MA"
case centralKurdish = "ckb"
case centralKurdishIran = "ckb_IR"
case centralKurdishIraq = "ckb_IQ"
case cherokee = "chr"
case cherokeeUnitedStates = "chr_US"
case chiga = "cgg"
case chigaUganda = "cgg_UG"
case chinese = "zh"
case chineseChina = "zh_Hans_CN"
case chineseHongKongSarChina = "zh_Hant_HK"
case chineseMacauSarChina = "zh_Hant_MO"
case chineseSimplified = "zh_Hans"
case chineseSimplifiedHongKongSarChina = "zh_Hans_HK"
case chineseSimplifiedMacauSarChina = "zh_Hans_MO"
case chineseSingapore = "zh_Hans_SG"
case chineseTaiwan = "zh_Hant_TW"
case chineseTraditional = "zh_Hant"
case colognian = "ksh"
case colognianGermany = "ksh_DE"
case cornish = "kw"
case cornishUnitedKingdom = "kw_GB"
case croatian = "hr"
case croatianBosniaHerzegovina = "hr_BA"
case croatianCroatia = "hr_HR"
case czech = "cs"
case czechCzechRepublic = "cs_CZ"
case danish = "da"
case danishDenmark = "da_DK"
case danishGreenland = "da_GL"
case duala = "dua"
case dualaCameroon = "dua_CM"
case dutch = "nl"
case dutchAruba = "nl_AW"
case dutchBelgium = "nl_BE"
case dutchCaribbeanNetherlands = "nl_BQ"
case dutchCuraao = "nl_CW"
case dutchNetherlands = "nl_NL"
case dutchSintMaarten = "nl_SX"
case dutchSuriname = "nl_SR"
case dzongkha = "dz"
case dzongkhaBhutan = "dz_BT"
case embu = "ebu"
case embuKenya = "ebu_KE"
case english = "en"
case englishAlbania = "en_AL"
case englishAmericanSamoa = "en_AS"
case englishAndorra = "en_AD"
case englishAnguilla = "en_AI"
case englishAntiguaBarbuda = "en_AG"
case englishAustralia = "en_AU"
case englishAustria = "en_AT"
case englishBahamas = "en_BS"
case englishBarbados = "en_BB"
case englishBelgium = "en_BE"
case englishBelize = "en_BZ"
case englishBermuda = "en_BM"
case englishBosniaHerzegovina = "en_BA"
case englishBotswana = "en_BW"
case englishBritishIndianOceanTerritory = "en_IO"
case englishBritishVirginIslands = "en_VG"
case englishCameroon = "en_CM"
case englishCanada = "en_CA"
case englishCaymanIslands = "en_KY"
case englishChristmasIsland = "en_CX"
case englishCocosKeelingIslands = "en_CC"
case englishCookIslands = "en_CK"
case englishCroatia = "en_HR"
case englishCyprus = "en_CY"
case englishCzechRepublic = "en_CZ"
case englishDenmark = "en_DK"
case englishDiegoGarcia = "en_DG"
case englishDominica = "en_DM"
case englishEritrea = "en_ER"
case englishEstonia = "en_EE"
case englishEurope = "en_150"
case englishFalklandIslands = "en_FK"
case englishFiji = "en_FJ"
case englishFinland = "en_FI"
case englishFrance = "en_FR"
case englishGambia = "en_GM"
case englishGermany = "en_DE"
case englishGhana = "en_GH"
case englishGibraltar = "en_GI"
case englishGreece = "en_GR"
case englishGrenada = "en_GD"
case englishGuam = "en_GU"
case englishGuernsey = "en_GG"
case englishGuyana = "en_GY"
case englishHongKongSarChina = "en_HK"
case englishHungary = "en_HU"
case englishIceland = "en_IS"
case englishIndia = "en_IN"
case englishIreland = "en_IE"
case englishIsleOfMan = "en_IM"
case englishIsrael = "en_IL"
case englishItaly = "en_IT"
case englishJamaica = "en_JM"
case englishJersey = "en_JE"
case englishKenya = "en_KE"
case englishKiribati = "en_KI"
case englishLatvia = "en_LV"
case englishLesotho = "en_LS"
case englishLiberia = "en_LR"
case englishLithuania = "en_LT"
case englishLuxembourg = "en_LU"
case englishMacauSarChina = "en_MO"
case englishMadagascar = "en_MG"
case englishMalawi = "en_MW"
case englishMalaysia = "en_MY"
case englishMalta = "en_MT"
case englishMarshallIslands = "en_MH"
case englishMauritius = "en_MU"
case englishMicronesia = "en_FM"
case englishMontenegro = "en_ME"
case englishMontserrat = "en_MS"
case englishNamibia = "en_NA"
case englishNauru = "en_NR"
case englishNetherlands = "en_NL"
case englishNewZealand = "en_NZ"
case englishNigeria = "en_NG"
case englishNiue = "en_NU"
case englishNorfolkIsland = "en_NF"
case englishNorthernMarianaIslands = "en_MP"
case englishNorway = "en_NO"
case englishPakistan = "en_PK"
case englishPalau = "en_PW"
case englishPapuaNewGuinea = "en_PG"
case englishPhilippines = "en_PH"
case englishPitcairnIslands = "en_PN"
case englishPoland = "en_PL"
case englishPortugal = "en_PT"
case englishPuertoRico = "en_PR"
case englishRomania = "en_RO"
case englishRussia = "en_RU"
case englishRwanda = "en_RW"
case englishSamoa = "en_WS"
case englishSeychelles = "en_SC"
case englishSierraLeone = "en_SL"
case englishSingapore = "en_SG"
case englishSintMaarten = "en_SX"
case englishSlovakia = "en_SK"
case englishSlovenia = "en_SI"
case englishSolomonIslands = "en_SB"
case englishSouthAfrica = "en_ZA"
case englishSouthSudan = "en_SS"
case englishSpain = "en_ES"
case englishStHelena = "en_SH"
case englishStKittsNevis = "en_KN"
case englishStLucia = "en_LC"
case englishStVincentGrenadines = "en_VC"
case englishSudan = "en_SD"
case englishSwaziland = "en_SZ"
case englishSweden = "en_SE"
case englishSwitzerland = "en_CH"
case englishTanzania = "en_TZ"
case englishTokelau = "en_TK"
case englishTonga = "en_TO"
case englishTrinidadTobago = "en_TT"
case englishTurkey = "en_TR"
case englishTurksCaicosIslands = "en_TC"
case englishTuvalu = "en_TV"
case englishUSOutlyingIslands = "en_UM"
case englishUSVirginIslands = "en_VI"
case englishUganda = "en_UG"
case englishUnitedKingdom = "en_GB"
case englishUnitedStates = "en_US"
case englishUnitedStatesComputer = "en_US_POSIX"
case englishVanuatu = "en_VU"
case englishWorld = "en_001"
case englishZambia = "en_ZM"
case englishZimbabwe = "en_ZW"
case esperanto = "eo"
case estonian = "et"
case estonianEstonia = "et_EE"
case ewe = "ee"
case eweGhana = "ee_GH"
case eweTogo = "ee_TG"
case ewondo = "ewo"
case ewondoCameroon = "ewo_CM"
case faroese = "fo"
case faroeseFaroeIslands = "fo_FO"
case filipino = "fil"
case filipinoPhilippines = "fil_PH"
case finnish = "fi"
case finnishFinland = "fi_FI"
case french = "fr"
case frenchAlgeria = "fr_DZ"
case frenchBelgium = "fr_BE"
case frenchBenin = "fr_BJ"
case frenchBurkinaFaso = "fr_BF"
case frenchBurundi = "fr_BI"
case frenchCameroon = "fr_CM"
case frenchCanada = "fr_CA"
case frenchCentralAfricanRepublic = "fr_CF"
case frenchChad = "fr_TD"
case frenchComoros = "fr_KM"
case frenchCongoBrazzaville = "fr_CG"
case frenchCongoKinshasa = "fr_CD"
case frenchCteDivoire = "fr_CI"
case frenchDjibouti = "fr_DJ"
case frenchEquatorialGuinea = "fr_GQ"
case frenchFrance = "fr_FR"
case frenchFrenchGuiana = "fr_GF"
case frenchFrenchPolynesia = "fr_PF"
case frenchGabon = "fr_GA"
case frenchGuadeloupe = "fr_GP"
case frenchGuinea = "fr_GN"
case frenchHaiti = "fr_HT"
case frenchLuxembourg = "fr_LU"
case frenchMadagascar = "fr_MG"
case frenchMali = "fr_ML"
case frenchMartinique = "fr_MQ"
case frenchMauritania = "fr_MR"
case frenchMauritius = "fr_MU"
case frenchMayotte = "fr_YT"
case frenchMonaco = "fr_MC"
case frenchMorocco = "fr_MA"
case frenchNewCaledonia = "fr_NC"
case frenchNiger = "fr_NE"
case frenchRunion = "fr_RE"
case frenchRwanda = "fr_RW"
case frenchSenegal = "fr_SN"
case frenchSeychelles = "fr_SC"
case frenchStBarthlemy = "fr_BL"
case frenchStMartin = "fr_MF"
case frenchStPierreMiquelon = "fr_PM"
case frenchSwitzerland = "fr_CH"
case frenchSyria = "fr_SY"
case frenchTogo = "fr_TG"
case frenchTunisia = "fr_TN"
case frenchVanuatu = "fr_VU"
case frenchWallisFutuna = "fr_WF"
case friulian = "fur"
case friulianItaly = "fur_IT"
case fulah = "ff"
case fulahCameroon = "ff_CM"
case fulahGuinea = "ff_GN"
case fulahMauritania = "ff_MR"
case fulahSenegal = "ff_SN"
case galician = "gl"
case galicianSpain = "gl_ES"
case ganda = "lg"
case gandaUganda = "lg_UG"
case georgian = "ka"
case georgianGeorgia = "ka_GE"
case german = "de"
case germanAustria = "de_AT"
case germanBelgium = "de_BE"
case germanGermany = "de_DE"
case germanLiechtenstein = "de_LI"
case germanLuxembourg = "de_LU"
case germanSwitzerland = "de_CH"
case greek = "el"
case greekCyprus = "el_CY"
case greekGreece = "el_GR"
case gujarati = "gu"
case gujaratiIndia = "gu_IN"
case gusii = "guz"
case gusiiKenya = "guz_KE"
case hausa = "ha_Latn"
case hausaGhana = "ha_Latn_GH"
case hausaNiger = "ha_Latn_NE"
case hausaNigeria = "ha_Latn_NG"
case hawaiian = "haw"
case hawaiianUnitedStates = "haw_US"
case hebrew = "he"
case hebrewIsrael = "he_IL"
case hindi = "hi"
case hindiIndia = "hi_IN"
case hungarian = "hu"
case hungarianHungary = "hu_HU"
case icelandic = "is"
case icelandicIceland = "is_IS"
case igbo = "ig"
case igboNigeria = "ig_NG"
case inariSami = "smn"
case inariSamiFinland = "smn_FI"
case indonesian = "id"
case indonesianIndonesia = "id_ID"
case inuktitut = "iu"
case inuktitutUnifiedCanadianAboriginalSyllabics = "iu_Cans"
case inuktitutUnifiedCanadianAboriginalSyllabicsCanada = "iu_Cans_CA"
case irish = "ga"
case irishIreland = "ga_IE"
case italian = "it"
case italianItaly = "it_IT"
case italianSanMarino = "it_SM"
case italianSwitzerland = "it_CH"
case japanese = "ja"
case japaneseJapan = "ja_JP"
case jolaFonyi = "dyo"
case jolaFonyiSenegal = "dyo_SN"
case kabuverdianu = "kea"
case kabuverdianuCapeVerde = "kea_CV"
case kabyle = "kab"
case kabyleAlgeria = "kab_DZ"
case kako = "kkj"
case kakoCameroon = "kkj_CM"
case kalaallisut = "kl"
case kalaallisutGreenland = "kl_GL"
case kalenjin = "kln"
case kalenjinKenya = "kln_KE"
case kamba = "kam"
case kambaKenya = "kam_KE"
case kannada = "kn"
case kannadaIndia = "kn_IN"
case kashmiri = "ks"
case kashmiriArabic = "ks_Arab"
case kashmiriArabicIndia = "ks_Arab_IN"
case kazakh = "kk_Cyrl"
case kazakhKazakhstan = "kk_Cyrl_KZ"
case khmer = "km"
case khmerCambodia = "km_KH"
case kikuyu = "ki"
case kikuyuKenya = "ki_KE"
case kinyarwanda = "rw"
case kinyarwandaRwanda = "rw_RW"
case konkani = "kok"
case konkaniIndia = "kok_IN"
case korean = "ko"
case koreanNorthKorea = "ko_KP"
case koreanSouthKorea = "ko_KR"
case koyraChiini = "khq"
case koyraChiiniMali = "khq_ML"
case koyraboroSenni = "ses"
case koyraboroSenniMali = "ses_ML"
case kwasio = "nmg"
case kwasioCameroon = "nmg_CM"
case kyrgyz = "ky_Cyrl"
case kyrgyzKyrgyzstan = "ky_Cyrl_KG"
case lakota = "lkt"
case lakotaUnitedStates = "lkt_US"
case langi = "lag"
case langiTanzania = "lag_TZ"
case lao = "lo"
case laoLaos = "lo_LA"
case latvian = "lv"
case latvianLatvia = "lv_LV"
case lingala = "ln"
case lingalaAngola = "ln_AO"
case lingalaCentralAfricanRepublic = "ln_CF"
case lingalaCongoBrazzaville = "ln_CG"
case lingalaCongoKinshasa = "ln_CD"
case lithuanian = "lt"
case lithuanianLithuania = "lt_LT"
case lowerSorbian = "dsb"
case lowerSorbianGermany = "dsb_DE"
case lubaKatanga = "lu"
case lubaKatangaCongoKinshasa = "lu_CD"
case luo = "luo"
case luoKenya = "luo_KE"
case luxembourgish = "lb"
case luxembourgishLuxembourg = "lb_LU"
case luyia = "luy"
case luyiaKenya = "luy_KE"
case macedonian = "mk"
case macedonianMacedonia = "mk_MK"
case machame = "jmc"
case machameTanzania = "jmc_TZ"
case makhuwaMeetto = "mgh"
case makhuwaMeettoMozambique = "mgh_MZ"
case makonde = "kde"
case makondeTanzania = "kde_TZ"
case malagasy = "mg"
case malagasyMadagascar = "mg_MG"
case malay = "ms_Latn"
case malayArabic = "ms_Arab"
case malayArabicBrunei = "ms_Arab_BN"
case malayArabicMalaysia = "ms_Arab_MY"
case malayBrunei = "ms_Latn_BN"
case malayMalaysia = "ms_Latn_MY"
case malaySingapore = "ms_Latn_SG"
case malayalam = "ml"
case malayalamIndia = "ml_IN"
case maltese = "mt"
case malteseMalta = "mt_MT"
case manx = "gv"
case manxIsleOfMan = "gv_IM"
case marathi = "mr"
case marathiIndia = "mr_IN"
case masai = "mas"
case masaiKenya = "mas_KE"
case masaiTanzania = "mas_TZ"
case meru = "mer"
case meruKenya = "mer_KE"
case meta = "mgo"
case metaCameroon = "mgo_CM"
case mongolian = "mn_Cyrl"
case mongolianMongolia = "mn_Cyrl_MN"
case morisyen = "mfe"
case morisyenMauritius = "mfe_MU"
case mundang = "mua"
case mundangCameroon = "mua_CM"
case nama = "naq"
case namaNamibia = "naq_NA"
case nepali = "ne"
case nepaliIndia = "ne_IN"
case nepaliNepal = "ne_NP"
case ngiemboon = "nnh"
case ngiemboonCameroon = "nnh_CM"
case ngomba = "jgo"
case ngombaCameroon = "jgo_CM"
case northNdebele = "nd"
case northNdebeleZimbabwe = "nd_ZW"
case northernSami = "se"
case northernSamiFinland = "se_FI"
case northernSamiNorway = "se_NO"
case northernSamiSweden = "se_SE"
case norwegianBokml = "nb"
case norwegianBokmlNorway = "nb_NO"
case norwegianBokmlSvalbardJanMayen = "nb_SJ"
case norwegianNynorsk = "nn"
case norwegianNynorskNorway = "nn_NO"
case nuer = "nus"
case nuerSudan = "nus_SD"
case nyankole = "nyn"
case nyankoleUganda = "nyn_UG"
case oriya = "or"
case oriyaIndia = "or_IN"
case oromo = "om"
case oromoEthiopia = "om_ET"
case oromoKenya = "om_KE"
case ossetic = "os"
case osseticGeorgia = "os_GE"
case osseticRussia = "os_RU"
case pashto = "ps"
case pashtoAfghanistan = "ps_AF"
case persian = "fa"
case persianAfghanistan = "fa_AF"
case persianIran = "fa_IR"
case polish = "pl"
case polishPoland = "pl_PL"
case portuguese = "pt"
case portugueseAngola = "pt_AO"
case portugueseBrazil = "pt_BR"
case portugueseCapeVerde = "pt_CV"
case portugueseGuineaBissau = "pt_GW"
case portugueseMacauSarChina = "pt_MO"
case portugueseMozambique = "pt_MZ"
case portuguesePortugal = "pt_PT"
case portugueseSoTomPrncipe = "pt_ST"
case portugueseTimorLeste = "pt_TL"
case punjabi = "pa_Guru"
case punjabiArabic = "pa_Arab"
case punjabiArabicPakistan = "pa_Arab_PK"
case punjabiIndia = "pa_Guru_IN"
case quechua = "qu"
case quechuaBolivia = "qu_BO"
case quechuaEcuador = "qu_EC"
case quechuaPeru = "qu_PE"
case romanian = "ro"
case romanianMoldova = "ro_MD"
case romanianRomania = "ro_RO"
case romansh = "rm"
case romanshSwitzerland = "rm_CH"
case rombo = "rof"
case romboTanzania = "rof_TZ"
case rundi = "rn"
case rundiBurundi = "rn_BI"
case russian = "ru"
case russianBelarus = "ru_BY"
case russianKazakhstan = "ru_KZ"
case russianKyrgyzstan = "ru_KG"
case russianMoldova = "ru_MD"
case russianRussia = "ru_RU"
case russianUkraine = "ru_UA"
case rwa = "rwk"
case rwaTanzania = "rwk_TZ"
case sakha = "sah"
case sakhaRussia = "sah_RU"
case samburu = "saq"
case samburuKenya = "saq_KE"
case sango = "sg"
case sangoCentralAfricanRepublic = "sg_CF"
case sangu = "sbp"
case sanguTanzania = "sbp_TZ"
case scottishGaelic = "gd"
case scottishGaelicUnitedKingdom = "gd_GB"
case sena = "seh"
case senaMozambique = "seh_MZ"
case serbian = "sr_Cyrl"
case serbianBosniaHerzegovina = "sr_Cyrl_BA"
case serbianKosovo = "sr_Cyrl_XK"
case serbianLatin = "sr_Latn"
case serbianLatinBosniaHerzegovina = "sr_Latn_BA"
case serbianLatinKosovo = "sr_Latn_XK"
case serbianLatinMontenegro = "sr_Latn_ME"
case serbianLatinSerbia = "sr_Latn_RS"
case serbianMontenegro = "sr_Cyrl_ME"
case serbianSerbia = "sr_Cyrl_RS"
case shambala = "ksb"
case shambalaTanzania = "ksb_TZ"
case shona = "sn"
case shonaZimbabwe = "sn_ZW"
case sichuanYi = "ii"
case sichuanYiChina = "ii_CN"
case sinhala = "si"
case sinhalaSriLanka = "si_LK"
case slovak = "sk"
case slovakSlovakia = "sk_SK"
case slovenian = "sl"
case slovenianSlovenia = "sl_SI"
case soga = "xog"
case sogaUganda = "xog_UG"
case somali = "so"
case somaliDjibouti = "so_DJ"
case somaliEthiopia = "so_ET"
case somaliKenya = "so_KE"
case somaliSomalia = "so_SO"
case spanish = "es"
case spanishArgentina = "es_AR"
case spanishBolivia = "es_BO"
case spanishCanaryIslands = "es_IC"
case spanishCeutaMelilla = "es_EA"
case spanishChile = "es_CL"
case spanishColombia = "es_CO"
case spanishCostaRica = "es_CR"
case spanishCuba = "es_CU"
case spanishDominicanRepublic = "es_DO"
case spanishEcuador = "es_EC"
case spanishElSalvador = "es_SV"
case spanishEquatorialGuinea = "es_GQ"
case spanishGuatemala = "es_GT"
case spanishHonduras = "es_HN"
case spanishLatinAmerica = "es_419"
case spanishMexico = "es_MX"
case spanishNicaragua = "es_NI"
case spanishPanama = "es_PA"
case spanishParaguay = "es_PY"
case spanishPeru = "es_PE"
case spanishPhilippines = "es_PH"
case spanishPuertoRico = "es_PR"
case spanishSpain = "es_ES"
case spanishUnitedStates = "es_US"
case spanishUruguay = "es_UY"
case spanishVenezuela = "es_VE"
case standardMoroccanTamazight = "zgh"
case standardMoroccanTamazightMorocco = "zgh_MA"
case swahili = "sw"
case swahiliCongoKinshasa = "sw_CD"
case swahiliKenya = "sw_KE"
case swahiliTanzania = "sw_TZ"
case swahiliUganda = "sw_UG"
case swedish = "sv"
case swedishlandIslands = "sv_AX"
case swedishFinland = "sv_FI"
case swedishSweden = "sv_SE"
case swissGerman = "gsw"
case swissGermanFrance = "gsw_FR"
case swissGermanLiechtenstein = "gsw_LI"
case swissGermanSwitzerland = "gsw_CH"
case tachelhit = "shi_Latn"
case tachelhitMorocco = "shi_Latn_MA"
case tachelhitTifinagh = "shi_Tfng"
case tachelhitTifinaghMorocco = "shi_Tfng_MA"
case taita = "dav"
case taitaKenya = "dav_KE"
case tajik = "tg_Cyrl"
case tajikTajikistan = "tg_Cyrl_TJ"
case tamil = "ta"
case tamilIndia = "ta_IN"
case tamilMalaysia = "ta_MY"
case tamilSingapore = "ta_SG"
case tamilSriLanka = "ta_LK"
case tasawaq = "twq"
case tasawaqNiger = "twq_NE"
case telugu = "te"
case teluguIndia = "te_IN"
case teso = "teo"
case tesoKenya = "teo_KE"
case tesoUganda = "teo_UG"
case thai = "th"
case thaiThailand = "th_TH"
case tibetan = "bo"
case tibetanChina = "bo_CN"
case tibetanIndia = "bo_IN"
case tigrinya = "ti"
case tigrinyaEritrea = "ti_ER"
case tigrinyaEthiopia = "ti_ET"
case tongan = "to"
case tonganTonga = "to_TO"
case turkish = "tr"
case turkishCyprus = "tr_CY"
case turkishTurkey = "tr_TR"
case turkmen = "tk_Latn"
case turkmenTurkmenistan = "tk_Latn_TM"
case ukrainian = "uk"
case ukrainianUkraine = "uk_UA"
case upperSorbian = "hsb"
case upperSorbianGermany = "hsb_DE"
case urdu = "ur"
case urduIndia = "ur_IN"
case urduPakistan = "ur_PK"
case uyghur = "ug"
case uyghurArabic = "ug_Arab"
case uyghurArabicChina = "ug_Arab_CN"
case uzbek = "uz_Cyrl"
case uzbekArabic = "uz_Arab"
case uzbekArabicAfghanistan = "uz_Arab_AF"
case uzbekLatin = "uz_Latn"
case uzbekLatinUzbekistan = "uz_Latn_UZ"
case uzbekUzbekistan = "uz_Cyrl_UZ"
case vai = "vai_Vaii"
case vaiLatin = "vai_Latn"
case vaiLatinLiberia = "vai_Latn_LR"
case vaiLiberia = "vai_Vaii_LR"
case vietnamese = "vi"
case vietnameseVietnam = "vi_VN"
case vunjo = "vun"
case vunjoTanzania = "vun_TZ"
case walser = "wae"
case walserSwitzerland = "wae_CH"
case welsh = "cy"
case welshUnitedKingdom = "cy_GB"
case westernFrisian = "fy"
case westernFrisianNetherlands = "fy_NL"
case yangben = "yav"
case yangbenCameroon = "yav_CM"
case yiddish = "yi"
case yiddishWorld = "yi_001"
case yoruba = "yo"
case yorubaBenin = "yo_BJ"
case yorubaNigeria = "yo_NG"
case zarma = "dje"
case zarmaNiger = "dje_NE"
case zulu = "zu"
case zuluSouthAfrica = "zu_ZA"
/// Return a valid `Locale` instance from currency locale enum
public var locale: Locale {
switch self {
case .current: return Locale.current
case .autoUpdating: return Locale.autoupdatingCurrent
default: return Locale(identifier: rawValue)
}
}
}

View File

@ -0,0 +1,18 @@
//
// NumberFormatter.swift
// CurrencyText
//
// Created by Felipe Lefèvre Marino on 12/27/18.
//
import Foundation
public extension NumberFormatter {
func string(from doubleValue: Double?) -> String? {
if let doubleValue = doubleValue {
return string(from: NSNumber(value: doubleValue))
}
return nil
}
}

View File

@ -0,0 +1,69 @@
//
// String.swift
// CurrencyText
//
// Created by Felipe Lefèvre Marino on 4/3/18.
// Copyright © 2018 Felipe Lefèvre Marino. All rights reserved.
//
import Foundation
public protocol CurrencyString {
var representsZero: Bool { get }
var hasNumbers: Bool { get }
var lastNumberOffsetFromEnd: Int? { get }
func numeralFormat() -> String
mutating func updateDecimalSeparator(decimalDigits: Int)
}
//Currency String Extension
extension String: CurrencyString {
// MARK: Properties
/// Informs with the string represents the value of zero
public var representsZero: Bool {
return numeralFormat().replacingOccurrences(of: "0", with: "").count == 0
}
/// Returns if the string does have any character that represents numbers
public var hasNumbers: Bool {
return numeralFormat().count > 0
}
/// The offset from end index to the index _right after_ the last number in the String.
/// e.g. For the String "123some", the last number position is 4, because from the _end index_ to the index of _3_
/// there is an offset of 4, "e, m, o and s".
public var lastNumberOffsetFromEnd: Int? {
guard let indexOfLastNumber = lastIndex(where: { $0.isNumber }) else { return nil }
let indexAfterLastNumber = index(after: indexOfLastNumber)
return distance(from: endIndex, to: indexAfterLastNumber)
}
// MARK: Functions
/// Updates a currency string decimal separator position based on
/// the amount of decimal digits desired
///
/// - Parameter decimalDigits: The amount of decimal digits of the currency formatted string
public mutating func updateDecimalSeparator(decimalDigits: Int) {
guard decimalDigits != 0 && count >= decimalDigits else { return }
let decimalsRange = index(endIndex, offsetBy: -decimalDigits)..<endIndex
let decimalChars = self[decimalsRange]
replaceSubrange(decimalsRange, with: "." + decimalChars)
}
/// The numeral format of a string - remove all non numerical ocurrences
///
/// - Returns: itself without the non numerical characters ocurrences
public func numeralFormat() -> String {
return replacingOccurrences(of:"[^0-9]", with: "", options: .regularExpression)
}
}
// MARK: - Static constants
extension String {
public static let negativeSymbol = "-"
}

View File

@ -0,0 +1,182 @@
//
// CurrencyUITextFieldDelegate.swift
// CurrencyText
//
// Created by Felipe Lefèvre Marino on 12/26/18.
// Copyright © 2018 Felipe Lefèvre Marino. All rights reserved.
//
import UIKit
/// Custom text field delegate, that formats user inputs based on a given currency formatter.
public class CurrencyUITextFieldDelegate: NSObject {
public var formatter: (CurrencyFormatting & CurrencyAdjusting)!
public var textUpdated: (() -> Void)?
/// Text field clears its text when value value is equal to zero.
public var clearsWhenValueIsZero: Bool = false
/// A delegate object to receive and potentially handle `UITextFieldDelegate events` that are sent to `CurrencyUITextFieldDelegate`.
///
/// Note: Make sure the implementation of this object does not wrongly interfere with currency formatting.
///
/// By returning `false` on`textField(textField:shouldChangeCharactersIn:replacementString:)` no currency formatting is done.
public var passthroughDelegate: UITextFieldDelegate? {
get { return _passthroughDelegate }
set {
guard newValue !== self else { return }
_passthroughDelegate = newValue
}
}
weak private(set) var _passthroughDelegate: UITextFieldDelegate?
public init(formatter: CurrencyFormatter) {
self.formatter = formatter
}
}
// MARK: - UITextFieldDelegate
extension CurrencyUITextFieldDelegate: UITextFieldDelegate {
@discardableResult
open func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
return passthroughDelegate?.textFieldShouldBeginEditing?(textField) ?? true
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
textField.setInitialSelectedTextRange()
passthroughDelegate?.textFieldDidBeginEditing?(textField)
}
@discardableResult
public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
if let text = textField.text, text.representsZero && clearsWhenValueIsZero {
textField.text = ""
}
else if let text = textField.text, let updated = formatter.formattedStringAdjustedToFitAllowedValues(from: text), updated != text {
textField.text = updated
}
return passthroughDelegate?.textFieldShouldEndEditing?(textField) ?? true
}
open func textFieldDidEndEditing(_ textField: UITextField) {
passthroughDelegate?.textFieldDidEndEditing?(textField)
}
@discardableResult
open func textFieldShouldClear(_ textField: UITextField) -> Bool {
return passthroughDelegate?.textFieldShouldClear?(textField) ?? true
}
@discardableResult
open func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return passthroughDelegate?.textFieldShouldReturn?(textField) ?? true
}
@discardableResult
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let shouldChangeCharactersInRange = passthroughDelegate?.textField?(textField,
shouldChangeCharactersIn: range,
replacementString: string) ?? true
guard shouldChangeCharactersInRange else {
return false
}
// Store selected text range offset from end, before updating and reformatting the currency string.
let lastSelectedTextRangeOffsetFromEnd = textField.selectedTextRangeOffsetFromEnd
// Before leaving the scope, update selected text range,
// respecting previous selected text range offset from end.
defer {
textField.updateSelectedTextRange(lastOffsetFromEnd: lastSelectedTextRangeOffsetFromEnd)
textUpdated?()
}
guard !string.isEmpty else {
handleDeletion(in: textField, at: range)
return false
}
guard string.hasNumbers else {
addNegativeSymbolIfNeeded(in: textField, at: range, replacementString: string)
return false
}
setFormattedText(in: textField, inputString: string, range: range)
return false
}
}
// MARK: - Private
extension CurrencyUITextFieldDelegate {
/// Verifies if user inputed a negative symbol at the first lowest
/// bound of the text field and add it.
///
/// - Parameters:
/// - textField: text field that user interacted with
/// - range: user input range
/// - string: user input string
private func addNegativeSymbolIfNeeded(in textField: UITextField, at range: NSRange, replacementString string: String) {
guard textField.keyboardType == .numbersAndPunctuation else { return }
if string == .negativeSymbol && textField.text?.isEmpty == true {
textField.text = .negativeSymbol
} else if range.lowerBound == 0 && string == .negativeSymbol &&
textField.text?.contains(String.negativeSymbol) == false {
textField.text = .negativeSymbol + (textField.text ?? "")
}
}
/// Correctly delete characters when user taps remove key.
///
/// - Parameters:
/// - textField: text field that user interacted with
/// - range: range to be removed
private func handleDeletion(in textField: UITextField, at range: NSRange) {
if var text = textField.text {
if let textRange = Range(range, in: text) {
text.removeSubrange(textRange)
} else {
text.removeLast()
}
if text.isEmpty {
textField.text = text
} else {
textField.text = formatter.formattedStringWithAdjustedDecimalSeparator(from: text)
}
}
}
/// Formats text field's text with new input string and changed range
///
/// - Parameters:
/// - textField: text field that user interacted with
/// - inputString: typed string
/// - range: range where the string should be added
private func setFormattedText(in textField: UITextField, inputString: String, range: NSRange) {
var updatedText = ""
if let text = textField.text {
if text.isEmpty {
updatedText = formatter.initialText + inputString
} else if let range = Range(range, in: text) {
updatedText = text.replacingCharacters(in: range, with: inputString)
} else {
updatedText = text.appending(inputString)
}
}
if updatedText.numeralFormat().count > formatter.maxDigitsCount {
updatedText.removeLast()
}
textField.text = formatter.formattedStringWithAdjustedDecimalSeparator(from: updatedText)
}
}

View File

@ -0,0 +1,61 @@
//
// UITextField.swift
// CurrencyText
//
// Created by Felipe Lefèvre Marino on 12/26/18.
//
import UIKit
public extension UITextField {
// MARK: Public
var selectedTextRangeOffsetFromEnd: Int {
return offset(from: endOfDocument, to: selectedTextRange?.end ?? endOfDocument)
}
/// Sets the selected text range when the text field is starting to be edited.
/// _Should_ be called when text field start to be the first responder.
func setInitialSelectedTextRange() {
// update selected text range if needed
adjustSelectedTextRange(lastOffsetFromEnd: 0) // at the end when first selected
}
/// Interface to update the selected text range as expected.
/// - Parameter lastOffsetFromEnd: The last stored selected text range offset from end. Used to keep it concise with pre-formatting.
func updateSelectedTextRange(lastOffsetFromEnd: Int) {
adjustSelectedTextRange(lastOffsetFromEnd: lastOffsetFromEnd)
}
// MARK: Private
/// Adjust the selected text range to match the best position.
private func adjustSelectedTextRange(lastOffsetFromEnd: Int) {
/// If text is empty the offset is set to zero, the selected text range does need to be changed.
if let text = text, text.isEmpty {
return
}
var offsetFromEnd = lastOffsetFromEnd
/// Adjust offset if needed. When the last number character offset from end is less than the current offset,
/// or in other words, is more distant to the end of the string, the offset is readjusted to it,
/// so the selected text range is correctly set to the last index with a number.
if let lastNumberOffsetFromEnd = text?.lastNumberOffsetFromEnd,
case let shouldOffsetBeAdjusted = lastNumberOffsetFromEnd < offsetFromEnd,
shouldOffsetBeAdjusted {
offsetFromEnd = lastNumberOffsetFromEnd
}
updateSelectedTextRange(offsetFromEnd: offsetFromEnd)
}
/// Update the selected text range with given offset from end.
private func updateSelectedTextRange(offsetFromEnd: Int) {
if let updatedCursorPosition = position(from: endOfDocument, offset: offsetFromEnd) {
selectedTextRange = textRange(from: updatedCursorPosition, to: updatedCursorPosition)
}
}
}

View File

@ -46,6 +46,55 @@ private func loadCurrencyFormatterEntries() -> [String: CurrencyFormatterEntry]
private let currencyFormatterEntries = loadCurrencyFormatterEntries()
public func setupCurrencyNumberFormatter(currency: String) -> NumberFormatter {
guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else {
preconditionFailure()
}
var result = ""
if entry.symbolOnLeft {
result.append("¤")
if entry.spaceBetweenAmountAndSymbol {
result.append(" ")
}
}
result.append("#")
result.append(entry.decimalSeparator)
for _ in 0 ..< entry.decimalDigits {
result.append("#")
}
if entry.decimalDigits != 0 {
result.append("0")
}
if !entry.symbolOnLeft {
if entry.spaceBetweenAmountAndSymbol {
result.append(" ")
}
result.append("¤")
}
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .currency
numberFormatter.positiveFormat = result
numberFormatter.negativeFormat = "-\(result)"
numberFormatter.currencySymbol = entry.symbol
numberFormatter.currencyDecimalSeparator = entry.decimalSeparator
numberFormatter.currencyGroupingSeparator = entry.thousandsSeparator
numberFormatter.minimumFractionDigits = entry.decimalDigits
numberFormatter.maximumFractionDigits = entry.decimalDigits
numberFormatter.minimumIntegerDigits = 1
return numberFormatter
}
public func fractionalToCurrencyAmount(value: Double, currency: String) -> Int64? {
guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else {
return nil
@ -54,7 +103,11 @@ public func fractionalToCurrencyAmount(value: Double, currency: String) -> Int64
for _ in 0 ..< entry.decimalDigits {
factor *= 10.0
}
return Int64(value * factor)
if value > Double(Int64.max) / factor {
return nil
} else {
return Int64(value * factor)
}
}
public func currencyToFractionalAmount(value: Int64, currency: String) -> Double? {