2
\$\begingroup\$

I have a price as a Double, which I should format it as a String.

Should I use an extension instead of classical class to encapsulate "formater", "serializers", etc? Or is there an even better alternative?

Using Class

class PriceFormatter{
    let value:Double

    init(_ value:Double){
        self.value = value
    }

    func format()->String{
        let strValue = "\(value)"
        let subStrings = strValue.split(separator: ".")
        let money = subStrings[0]
        var cents = "00"
        if subStrings.count > 1{
            cents = String(subStrings[1])

            if(cents.count == 1){
                cents = "0\(cents)"
            }
        }

        return "$\(money).\(cents)"
    }
}

Using Extension

extension Double {
    var priceFormatted:String{
        let strValue = "\(self)"
        let subStrings = strValue.split(separator: ".")
        let money = subStrings[0]
        var cents = "00"
        if subStrings.count > 1{
            cents = String(subStrings[1])

            if(cents.count == 1){
                cents = "0\(cents)"
            }
        }

        return "$\(money).\(cents)"
    }
}

Implementation

let input:Double = 5.0
let priceFromClass = PriceFormatter(input).format()
let priceFromExtension = input.priceFormatted
\$\endgroup\$
4
\$\begingroup\$

Should I use an extension instead of classical class to encapsulate "formatter", "serializers", etc?

No, it should not be an extension to the numeric type.

A couple of observations:

  • This formatter should really work with a variety of types (Double, Float, Decimal, etc.), so burying it inside a particular numeric type’s definition isn’t right. Theoretically you could consider an extension to a protocol, e.g. Numeric, instead, but attaching it to a particular type is certainly not right.

  • Formatters often have properties of their own. E.g. for a currency formatter, you’d probably have a locale for number formatting, currency symbol, preferred decimal places for that currency, etc., so that speaks to having the formatter as a type, itself, not just some computed property for the numeric type that is being displayed. It also affords the idea of having a single instance of a formatter that you use repeatedly for every underlying object that needs to be displayed.

  • Note, once you realize that you’re likely to want to configure a formatter once and use it repeatedly, it no longer makes sense to make the value a property of this formatter type. It should be a parameter that we pass to our method for creating a string representation.

  • By the way, formatters are not just “represent this object as a string”, but also “parse this string into an object” (and sometimes even “is this a valid substring for this type” which you might use to validate input as the user enters it). Once you consider formatters in this context, the notion of burying this in the underlying numeric types starts to run afoul of the “single responsibility principle”. A formatter is, effectively, an object for translating model objects to and from presentations in the UI.

  • For all of the aforementioned reasons, Foundation has a long tradition of separating data types and their formatters. See Formatter. And there are a litany of existing formatters. You should have a fairly compelling reason before deviating from this well established pattern.

  • Needless to say, I wouldn’t advise writing your own currency formatter at all when there are existing formatters that do the job very well. As Apple says:

    Before you decide to create a custom formatter, make sure that you cannot configure the public subclasses to satisfy your requirements.

    I’d suggest just using NumberFormatter with a numberStyle of one of the currency types (e.g. .currency, .currencyAccounting, etc.).

    But I assume this is for illustrative purposes only.

  • While your code snippet was a “formatter”, you mention serialization.

    That’s a different situation where you are often serializing objects with multiple properties of different types. And the serialization is often being handled by an existing serializer objects (e.g., for encoding, JSONEncoder and PropertyListEncoder, etc.). To support serialization for your custom types, you define your type to conform to the Codable protocol.

  • While presentation and parsing of values in the UI is often handled by separate formatter objects, it’s worth noting that there are a few string representations that we might add to our custom types for debugging purposes. For example, we might conform to CustomStringConvertible so that we can print our values. Value types (struct) have a decent default string representation, but especially for reference types (class) it can be nice to customize this.

    But don’t be tempted to use description as a backdoor for formatting strings in your UI. As Apple says,

    Accessing a type’s description property directly or using CustomStringConvertible as a generic constraint is discouraged.

    But for diagnostic purposes, this can be useful. Also see CustomDebugStringConvertible.

\$\endgroup\$
3
\$\begingroup\$

Correctness

  • Your code supposes that the decimal separator is ., which is not correct for all locales;
  • You are ignoring the grouping separator for thousands and so on: "$1000000.00" instead of "$1,000,000.00";
  • Negative amounts are not well formatted: "$-100.00" instead of "-$100.00";
  • Other than being slow, String interpolation represents really large amounts using the scientific notation :

    let input: Double = 10_000_000_000_000_000
    

Which would yield "$1e+16.00" but the right format is "$10,000,000,000,000,000.00" .

Alternative Solution

  • A PriceFormatter shouldn't own 1 value. Its job is to format, not be linked to a certain value.
  • There is no need to pollute the Double type with that extension. A number is a number.

Converting a number that represents an amount of money, in a certain currency, in a given locale, into a String, is the job of NumberFormatter :

let cf = NumberFormatter()
cf.numberStyle = .currency

Other properties can be set to your liking :

cf.maximumFractionDigits = 2
cf.minimumFractionDigits = 2
cf.locale = Locale(identifier: "en_US")
cf.decimalSeparator = "."
cf.groupingSeparator = ","

And use it like so:

let input: Double = 100_000_000

if let s = cf.string(for: input) {
    print(s)  //$100,000,000.00
}

To avoid creating new NumberFormatters whenever you need them, you could define a static property on NumberFormatter or Formatter (since string(for:) is defined on Formatter) :

extension Formatter {
    static var currencyFormatter: NumberFormatter {
        let cf = NumberFormatter()
        cf.numberStyle = .currency
        return cf
    }
}

And use it like so :

let str = Formatter.currencyFormatter.string(for: input)

if let s = str {
    print(s)  //$100,000,000.00
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Thank you. Your explanation about Formatter was so helpfull, because that I will improve my question. \$\endgroup\$ – ViTUu Mar 5 at 17:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.