1
\$\begingroup\$

I'm working on a clientside iOS app for my school that involves students purchasing things, and I wrote a String extension for formatting a String into USD. Is there a way to improve this or make it more "swifty"?

extension String {
    var usdFormat: String {
        var str = self
        var postDec = str.components(separatedBy: ".")
        switch postDec[1].count {
        case 0: str.append("00")
        case 1: str.append("0")
        default: break
        }

        str = "$" + str

        return str
    }
}
\$\endgroup\$
2
\$\begingroup\$

... for formatting a String into USD ...

A string is not the appropriate type to hold a monetary amount. What would your extension method return if called on the string "abc" or "?🤬" ? A price is a number and should be stored as such. Possible choices are

  • An integer holding the price in units of the smallest denomination (e.g. cents), or
  • A Decimal which represents a base-10 number with up do 38 decimal digits.

Creating a textual representation of a number is the job for a NumberFormatter. It has options to specify the desired number of fractional digits, and even has a dedicated .currency style.

Using a number formatter has several advantages over “manually” formatting the price, such as:

  • It chooses the appropriate style according to the user's locale.
  • It puts the currency symbol at the correct position.
  • It rounds the number to the correct number of decimal places.

Formatting a US dollar currency value is as simple as

let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = "USD"

let usPrice: Decimal = 1234.56
let displayString = formatter.string(for: usPrice)!
print(displayString)

This produces the string $1,234.56 if the user's locale is "en_US". On my device (with a german locale) it produces 1.234,56 $.

\$\endgroup\$
0
\$\begingroup\$

I would begin with more descriptive variable names. Something like formattedString and splitComponents would work in this case. The postDec or splitComponents variable is never mutated so that can be declared as a let.

extension String {
    var usdFormatted: String {
        var formattedString = self
        let splitComponents = self.components(separatedBy: ".")
    }
}

Next I would want to address a couple issues with the extension. The first glaring issue is that if you call this on a value that isn't numeric or separated by a period, it will crash. For this, I would recommend a guard statement before we begin doing any processing. This will provide a quick exit when a value is found that we don't want to use.

extension String {
    var usdFormatted: String {
        var formattedString = self
        guard let currency = Double(self) else {
            return formattedString
        }

        let splitComponents = self.components(separatedBy: ".")
    }
}

Now that we have addressed cases like ABC and 14FOO, we can look forward to handling the other logic that you have provided.

This also provides an interesting point where we now have a floating point value to work with. We can now take a shortcut and work directly with that floating point value. We can convert it to a string and specify the level of accuracy that we would like from that value. This can be done using String(format: "%.2f", currency). The format string signifies that we are providing a floating point value %f, and by passing in the .2 we can specify the precision for the string.

That will leave us with this:

extension String {
    var usdFormatted: String {
        var formattedString = self
        guard let currency = Double(self) else {
            return formattedString
        }

        formattedString =  "$" + String(format: "%.2f", currency)
        return formattedString
    }
}

A little bit of cleanup can be done since formattedString is unnecessary at this point, so that can be removed and you would end up with this extension:

extension String {
    var usdFormatted: String {          
        guard let currency = Double(self) else {
            return self
        }

        return "$" + String(format: "%.2f", currency)
    }
}

The only thing to note that would differ from this change is that it would provide rounding. If you did not want rounding to round up for any reason in this, you could accomplish this by adjusting the return statement like this return "$" + String(format: "%.2f", currency.rounded(.down)).

\$\endgroup\$
  • \$\begingroup\$ I think that using a Decimal may be more appropriate than a Double as the intermediate representation. \$\endgroup\$ – 200_success Oct 2 '18 at 20:52

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.