TL;DR Xcode Playground or Examples as Gist

The venerable (NS)Formatter class (and Apple’s various subclasses) are an Objective-C based API that is most well known as the go-to method for converting data types into strings. One of the lesser-known features of the APIs are that these same formatters can do the reverse: parse strings into their respective data types.

Apple’s modern Swift replacement system for Formatter is a set of protocols: FormatStyle and ParseableFormatStyle. The former handles the conversion to strings, and the latter strings to data.

One small thing. I mention conversion to and from strings here specifically. But these two protocols are completely type agnostic. You can convert to and from any data type. Follow your dreams.

FormatStyle and it’s various implementations is it’s own beast. Apple’s various implementations to support the built-in Foundation data types is quite extensive but spottily documented. I made a whole site to help you use them.

But that’s not what we’re going to talk about today.

Today we’re going to talk about ParseableFormatStyle and it’s implementations. How can we convert some strings into data?

What is ParseableFormatStyle Anyway?

The ParseableFormatStyle protocol is quite simple, it inherits from FormatStyle and simply defines a ParseStrategy property:

/// A type that can convert a given data type into a representation.
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
public protocol ParseableFormatStyle : FormatStyle {

    associatedtype Strategy : ParseStrategy where Self.FormatInput == Self.Strategy.ParseOutput, Self.FormatOutput == Self.Strategy.ParseInput

    /// A `ParseStrategy` that can be used to parse this `FormatStyle`'s output
    var parseStrategy: Self.Strategy { get }
}

Apple’s Documentation for ParseableFormatStyle

Okay, so what’s ParseStrategy then?

/// A type that can parse a representation of a given data type.
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
public protocol ParseStrategy : Decodable, Encodable, Hashable {

    /// The type of the representation describing the data.
    associatedtype ParseInput

    /// The type of the data type.
    associatedtype ParseOutput

    /// Creates an instance of the `ParseOutput` type from `value`.
    func parse(_ value: Self.ParseInput) throws -> Self.ParseOutput
}

Apple’s Documentation for ParseStrategy

The protocols themselves are concise, and to the point. You can very easily use them to bolt this functionality onto your own custom types.

How Do I Use It?

The most direct way of parsing a string into it’s respective data type is to create an instance of a ParseableFormatStyle that’s set up to understand the structure of the incoming string. From there you access it’s parseStrategy property, and call the parse() method on it.

This is a bit cumbersome, so Apple has included custom initializers onto each of the supported data types that take the string and either a ParseableFormatStyle or a ParseStrategy instance to do the parsing. What’s interesting is that Apple includes initializers that can accept any input type, as long as you provide a ParseStrategy that informs the type how to parse it. Aren’t constrained generics neat?

What Types Are Supported?

You can parse:

  • Dates
  • Decimals (Numbers, Percentages, Currency)
  • Person Names
  • URLs (iOS 16 only)

In general, you have two ways of accessing the parsing code:

Parsing Numbers

All of Swift’s numerical styles are supported with a new initializer.

// MARK: Parsing Integers
try? Int("120", format: .number) // 120
try? Int("0.25", format: .number) // 0
try? Int("1E5", format: .number.notation(.scientific)) // 100000

// MARK: Parsing Floating Point Numbers
try? Double("0.0025", format: .number) // 0.0025
try? Double("95%", format: .number) // 95
try? Double("95%", format: .percent) // 95
try? Double("1E5", format: .number.notation(.scientific)) // 100000

try? Float("0.0025", format: .number) // 0.0025
try? Float("95%", format: .number) // 95
try? Float("1E5", format: .number.notation(.scientific)) // 100000

// MARK: Parsing Decimals
try? Decimal("0.0025", format: .number) // 0.0025
try? Decimal("95%", format: .number) // 95
try? Decimal("1E5", format: .number.notation(.scientific)) // 100000

// MARK: Parsing Percentages
try? Int("98%", format: .percent) // 98
try? Float("95%", format: .percent) // 0.95
try? Decimal("95%", format: .percent) // 0.95

// MARK: Parsing Currencies
try? Decimal("$100.25", format: .currency(code: "USD")) // 100.25
try? Decimal("100.25 British Points", format: .currency(code: "GBP")) // 100.25

Parsing Dates

While there’s a myriad of different ways to format a Date object for display using the various included format styles. The only two that conform to ParseableFormatStyle are Date.FormatStyle and Date.ISO8601FormatStyle.

try? Date.FormatStyle()
    .day()
    .month()
    .year()
    .hour()
    .minute()
    .second()
    .parse("Feb 22, 2022, 2:22:22 AM") // Feb 22, 2022, 2:22:22 AM

try? Date.FormatStyle()
    .day()
    .month()
    .year()
    .hour()
    .minute()
    .second()
    .parseStrategy.parse("Feb 22, 2022, 2:22:22 AM") // Feb 22, 2022, 2:22:22 AM

try? Date.ISO8601FormatStyle(timeZone: TimeZone(secondsFromGMT: 0)!)
    .year()
    .day()
    .month()
    .dateSeparator(.dash)
    .dateTimeSeparator(.standard)
    .timeSeparator(.colon)
    .timeZoneSeparator(.colon)
    .time(includingFractionalSeconds: true)
    .parse("2022-02-22T09:22:22.000") // Feb 22, 2022, 2:22:22 AM

try? Date.ISO8601FormatStyle(timeZone: TimeZone(secondsFromGMT: 0)!)
    .year()
    .day()
    .month()
    .dateSeparator(.dash)
    .dateTimeSeparator(.standard)
    .timeSeparator(.colon)
    .timeZoneSeparator(.colon)
    .time(includingFractionalSeconds: true)
    .parseStrategy.parse("2022-02-22T09:22:22.000") // Feb 22, 2022, 2:22:22 AM

try? Date(
    "Feb 22, 2022, 2:22:22 AM",
    strategy: Date.FormatStyle().day().month().year().hour().minute().second().parseStrategy
) // Feb 22, 2022 at 2:22 AM

try? Date(
    "2022-02-22T09:22:22.000",
    strategy: Date.ISO8601FormatStyle(timeZone: TimeZone(secondsFromGMT: 0)!)
        .year()
        .day()
        .month()
        .dateSeparator(.dash)
        .dateTimeSeparator(.standard)
        .timeSeparator(.colon)
        .timeZoneSeparator(.colon)
        .time(includingFractionalSeconds: true)
        .parseStrategy
) // Feb 22, 2022 at 2:22 AM

Parsing Names

Parsing Names is helpful when you just don’t want to think about how various locals handle the order and display of given and family names.

// namePrefix: Dr givenName: Elizabeth middleName: Jillian familyName: Smith nameSuffix: Esq.
try? PersonNameComponents.FormatStyle()
    .parseStrategy.parse("Dr Elizabeth Jillian Smith Esq.")

// namePrefix: Dr givenName: Elizabeth middleName: Jillian familyName: Smith nameSuffix: Esq.
try? PersonNameComponents.FormatStyle(style: .long)
    .parseStrategy.parse("Dr Elizabeth Jillian Smith Esq.")

// namePrefix: Dr givenName: Elizabeth middleName: Jillian familyName: Smith nameSuffix: Esq.
try? PersonNameComponents.FormatStyle(style: .long, locale: Locale(identifier: "zh_CN"))
    .parseStrategy.parse("Dr Smith Elizabeth Jillian Esq.")

// namePrefix: Dr givenName: Elizabeth middleName: Jillian familyName: Smith nameSuffix: Esq.
try? PersonNameComponents.FormatStyle(style: .long)
    .locale(Locale(identifier: "zh_CN"))
    .parseStrategy.parse("Dr Smith Elizabeth Jillian Esq.")

// namePrefix: Dr givenName: Elizabeth middleName: Jillian familyName: Smith nameSuffix: Esq.
try? PersonNameComponents(
    "Dr Elizabeth Jillian Smith Esq.",
    strategy: PersonNameComponents.FormatStyle(style: .long).parseStrategy
)

URLs (iOS 16/Xcode 14 only)

Xcode 14, you can now use the new URL.FormatStyle.ParseStrategy struct to parse URLs (as an alternative to using the venerable URL(string:relativeTo) initializer).

You can set as options for each component to be required, optional, or default to a set value:

try URL.FormatStyle.Strategy(port: .defaultValue(80)).parse("http://www.apple.com") // http://www.apple.com:80
try URL.FormatStyle.Strategy(port: .optional).parse("http://www.apple.com") // http://www.apple.com
try URL.FormatStyle.Strategy(port: .required).parse("http://www.apple.com") // throws an error

// This returns a valid URL
try URL.FormatStyle.Strategy()
    .scheme(.required)
    .user(.required)
    .password(.required)
    .host(.required)
    .port(.required)
    .path(.required)
    .query(.required)
    .fragment(.required)
    .parse("https://jAppleseed:Test1234@apple.com:80/macbook-pro?get-free#someFragmentOfSomething")

// This throws an error (the port is missing)
try URL.FormatStyle.Strategy()
    .scheme(.required)
    .user(.required)
    .password(.required)
    .host(.required)
    .port(.required)
    .path(.required)
    .query(.required)
    .fragment(.required)
    .parse("https://jAppleseed:Test1234@apple.com/macbook-pro?get-free#someFragmentOfSomething")

By default, only the scheme and host are required.


Xcode Playground or Examples as Gist