2021-04-02 21:19:54 +04:00

346 lines
13 KiB
Swift

//
// 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
}
}