When consuming an API response, we’ll often see an ISO 8601 formatted date String like this:

2020-04-20T16:20:42+02:00

As a String, it’s not really useful for us, as it’s not really something you want to show your users. We may want to be able to extract just the hour, calculate the time interval between this date and others, or just format it in a way that’s more meaningful for the user. To do that, we can convert the String to a Date object.

Let’s add an extension to String with a new function asDate(). I like doing this so we don’t need to add additional classes or add more to the global namespace.

We can start by adding some tests:

class String_ExtensionsTests: XCTestCase {
    func testAsDate_invalid_notIso() throws {
        let invalidString = "2020-04-20 16:20:42 GMT+02:00"
        let date = invalidString.asDate()
        XCTAssertNil(date, "should not have converted string to date")
    }

    func testAsDate_invalid_missingTimeDelimiter() throws {
        let missingTimeDelimiterString = "2020-04-20 16:20:42+02:00"
        let date = missingTimeDelimiterString.asDate()
        XCTAssertNil(date, "should not have converted string to date")
    }
    
    func testAsDate_valid() throws {
        let isoDateString = "2020-04-20T16:20:42+02:00"
        
        let date = isoDateString.asDate()
        
        XCTAssertNotNil(date, "should have converted string to date")
        
        let calendar = Calendar(identifier: .gregorian)
        let twoHoursInSeconds = 60 * 60 * 2
        let amsterdamTimeZone = TimeZone(secondsFromGMT: twoHoursInSeconds)!
        let components = calendar.dateComponents(in: amsterdamTimeZone, from: date!)
        XCTAssertEqual(components.year, 2020, "has correct year")
        XCTAssertEqual(components.month, 4, "has correect month")
        XCTAssertEqual(components.day, 20, "has correct day 🌲")
        XCTAssertEqual(components.hour, 16, "has correct hour")
        XCTAssertEqual(components.minute, 20, "has correct minute")
        XCTAssertEqual(components.second, 42, "has correct second")
        XCTAssertEqual(components.timeZone, amsterdamTimeZone, "has correct time zone")
    }
}

Some things to note here:

  • Even though we’re likely OK with using Calendar.current, I like being as explicit as possible in my tests, since we don’t always know if the device running our tests will actually be set to a Gregorian calendar (but, honestly, it’ll likely be so).
  • I’ve set the time zone to match the String we’re testing. This just makes it more clear and easy to understand without having to do additional math in my own head when asserting (or if the tests fail in the future).

Great, with our tests failing, it’s time to implement the actual code and get those checkmarks green. Luckily for us, Swift gives us ISO8601DateFormatter. This makes things super easy for us:

extension String {
    func asDate() -> Date? {
        let formatter = ISO8601DateFormatter()
        return formatter.date(from: self)
    }
}

What? That’s it? Yup. Not a whole lot of code (vs our test code), but we can be sure we’re handling date Strings properly when we encounter them. Moreover, we’ve documented how we expect asDate should work, so other developers on our team know what to expect when using it.

🌲🌲🌲