Blog

Numeric string representations


In this post, we’ll create string representations of numeric types in Swift and extend these types with convenience functionality to make them easier to use.

The basics

When you create a string from a serializable type, you can use String(format:,) to provide rules for how you want the string to be formatted. Different formats apply to different value types.

For instance, you can create a two decimal string from a Double value like this:

let value = 1.2345
let result = String(format: "%0.2f", value)    // => "1.23"

While this is easy, it’s pretty hard to remember formats. In my opinion, it’s also nasty to scatter magic formatting strings all over the code base.

Extending numeric types

To make serializing a decimal value with any number of decimals easier, we could create extensions for the numeric types that we want to support:

public extension CGFloat {
    
    func string(withDecimals decimals: Int) -> String {
        String(format: .decimals(decimals), self)
    }
}

public extension Double {
    
    func string(withDecimals decimals: Int) -> String {
        String(format: .decimals(decimals), self)
    }
}

public extension Float {
    
    func string(withDecimals decimals: Int) -> String {
        String(format: .decimals(decimals), self)
    }
}

private extension String {
    
    static func decimals(_ decimals: Int) -> String { "%0.\(decimals)f" }
}

While this works, it’s repeating the same code over and over. We can do better.

Creating a shared extension

If we look at String(format:,), we can see that it takes a list of CVarArg arguments. It turns out that this is a protocol that is implemented by all the numeric types above.

We could thus make the extension above more general by applying it to CVarArg instead:

public extension CVarArg {
    
    func string(withDecimals decimals: Int) -> String {
        String(format: "%0.\(decimals)f", self)
    }
}

However, CVarArg is implemented by a bunch of types, where “decimals” doesn’t make sense. For instance, with the extension above, we could do this:

let string = "Hello, world!"
let result = string.string(withDecimals: 2)

While this is exciting and wild, it just doesn’t make sense. We need to restrict this somehow and can do this by introducing a new protocol:

public protocol NumericStringRepresentable: CVarArg {}

then let the numeric types we want to support implement this protocol:

extension CGFloat: NumericStringRepresentable {}
extension Double: NumericStringRepresentable {}
extension Float: NumericStringRepresentable {}

then apply the extension to this protocol instead of CVarArg:

public extension NumericStringRepresentable {
    
    func string(withDecimals decimals: Int) -> String {
        String(format: "%0.\(decimals)f", self)
    }
}

We have now constrained the extension to types that implement NumericStringRepresentable.

Source code

I have added these extensions to my SwiftKit library. You can find the source code here. Feel free to try it out and let me know what you think!