Categories
Foundation iOS Swift

How to keep Date’s microseconds precision in Swift

DateFormatter is used for converting string representation of date and time to a Date type and visa-versa. Something to be aware of is that the conversion loses microseconds precision. This is extremely important if we use these Date values for sorting and therefore ending up with incorrect order. Let’s consider an iOS app which uses API for fetching a list of items and each of the item contains a timestamp used for sorting the list. Often, these timestamps have the ISO8601 format like 2024-09-21T10:32:32.113123Z. Foundation framework has a dedicated formatter for parsing these strings: ISO8601DateFormatter. It is simple to use:

let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let date = formatter.date(from: "2024-09-21T10:32:32.113123Z")
print(date?.timeIntervalSince1970) // 1726914752.113
view raw ISO8601.swift hosted with ❤ by GitHub

Great, but there is on caveat, it ignores microseconds. Fortunately this can be fixed by manually parsing microseconds and adding the missing precision to the converted Date value. Here is an example, how to do this using an extension.

extension ISO8601DateFormatter {
func microsecondsDate(from dateString: String) -> Date? {
guard let millisecondsDate = date(from: dateString) else { return nil }
guard let fractionIndex = dateString.lastIndex(of: ".") else { return millisecondsDate }
guard let tzIndex = dateString.lastIndex(of: "Z") else { return millisecondsDate }
guard let startIndex = dateString.index(fractionIndex, offsetBy: 4, limitedBy: tzIndex) else { return millisecondsDate }
// Pad the missing zeros at the end and cut off nanoseconds
let microsecondsString = dateString[startIndex..<tzIndex].padding(toLength: 3, withPad: "0", startingAt: 0)
guard let microseconds = TimeInterval(microsecondsString) else { return millisecondsDate }
return Date(timeIntervalSince1970: millisecondsDate.timeIntervalSince1970 + microseconds / 1_000_000.0)
}
}
view raw ISO8601.swift hosted with ❤ by GitHub

That this code does is first converting the string using the original date(from:) method, followed by manually extracting digits for microseconds by handling cases where there are less than 3 digits or event there are nanoseconds present. Lastly a new Date value is created with the microseconds precision. Here are examples of the output (note that float’s precision comes into play).

let dateStrings = [
"2024-09-21T10:32:32.113Z",
"2024-09-21T10:32:32.1131Z",
"2024-09-21T10:32:32.11312Z",
"2024-09-21T10:32:32.113123Z",
"2024-09-21T10:32:32.1131234Z",
"2024-09-21T10:32:32.11312345Z",
"2024-09-21T10:32:32.113123456Z"
]
let dates = dateStrings.compactMap(formatter.microsecondsDate(from:))
for (string, date) in zip(dateStrings, dates) {
print(string, "->", date.timeIntervalSince1970)
}
/*
2024-09-21T10:32:32.113Z -> 1726914752.113
2024-09-21T10:32:32.1131Z -> 1726914752.1130998
2024-09-21T10:32:32.11312Z -> 1726914752.1131198
2024-09-21T10:32:32.113123Z -> 1726914752.113123
2024-09-21T10:32:32.1131234Z -> 1726914752.113123
2024-09-21T10:32:32.11312345Z -> 1726914752.113123
2024-09-21T10:32:32.113123456Z -> 1726914752.113123
*/
view raw ISO8601.swift hosted with ❤ by GitHub

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.