Numeric string representations

Jun 3, 2020 · Follow on Twitter and Mastodon swiftextensions

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!

Discussions & More

Please share any ideas, feedback or comments you may have in the Disqus section below, or by replying on Twitter or Mastodon..

If you found this text interesting, make sure to follow me on Twitter and Mastodon for more content like this, and to be notified when new content is published.

If you like & want to support my work, please consider sponsoring me on GitHub Sponsors.