Swift's JSONDecoder with multiple date formats in a JSON string? - swift

Swift's JSONDecoder offers a dateDecodingStrategy property, which allows us to define how to interpret incoming date strings in accordance with a DateFormatter object.
However, I am currently working with an API that returns both date strings (yyyy-MM-dd) and datetime strings (yyyy-MM-dd HH:mm:ss), depending on the property. Is there a way to have the JSONDecoder handle this, since the provided DateFormatter object can only deal with a single dateFormat at a time?
One ham-handed solution is to rewrite the accompanying Decodable models to just accept strings as their properties and to provide public Date getter/setter variables, but that seems like a poor solution to me. Any thoughts?

Please try decoder configurated similarly to this:
lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let dateStr = try container.decode(String.self)
// possible date strings: "2016-05-01", "2016-07-04T17:37:21.119229Z", "2018-05-20T15:00:00Z"
let len = dateStr.count
var date: Date? = nil
if len == 10 {
date = dateNoTimeFormatter.date(from: dateStr)
} else if len == 20 {
date = isoDateFormatter.date(from: dateStr)
} else {
date = self.serverFullDateFormatter.date(from: dateStr)
}
guard let date_ = date else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateStr)")
}
print("DATE DECODER \(dateStr) to \(date_)")
return date_
})
return decoder
}()

There are a few ways to deal with this:
You can create a DateFormatter subclass which first attempts the date-time string format, then if it fails, attempts the plain date format
You can give a .custom Date decoding strategy wherein you ask the Decoder for a singleValueContainer(), decode a string, and pass it through whatever formatters you want before passing the parsed date out
You can create a wrapper around the Date type which provides a custom init(from:) and encode(to:) which does this (but this isn't really any better than a .custom strategy)
You can use plain strings, as you suggest
You can provide a custom init(from:) on all types which use these dates and attempt different things in there
All in all, the first two methods are likely going to be the easiest and cleanest — you'll keep the default synthesized implementation of Codable everywhere without sacrificing type safety.

try this. (swift 4)
let formatter = DateFormatter()
var decoder: JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
if let date = formatter.date(from: dateString) {
return date
}
formatter.dateFormat = "yyyy-MM-dd"
if let date = formatter.date(from: dateString) {
return date
}
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Cannot decode date string \(dateString)")
}
return decoder
}

Swift 5
Actually based on #BrownsooHan version using a JSONDecoder extension
JSONDecoder+dateDecodingStrategyFormatters.swift
extension JSONDecoder {
/// Assign multiple DateFormatter to dateDecodingStrategy
///
/// Usage :
///
/// decoder.dateDecodingStrategyFormatters = [ DateFormatter.standard, DateFormatter.yearMonthDay ]
///
/// The decoder will now be able to decode two DateFormat, the 'standard' one and the 'yearMonthDay'
///
/// Throws a 'DecodingError.dataCorruptedError' if an unsupported date format is found while parsing the document
var dateDecodingStrategyFormatters: [DateFormatter]? {
#available(*, unavailable, message: "This variable is meant to be set only")
get { return nil }
set {
guard let formatters = newValue else { return }
self.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
for formatter in formatters {
if let date = formatter.date(from: dateString) {
return date
}
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)")
}
}
}
}
It is a bit of a hacky way to add a variable that can only be set, but you can easily transform var dateDecodingStrategyFormatters by func setDateDecodingStrategyFormatters(_ formatters: [DateFormatter]? )
Usage
lets say that you have already defined several DateFormatters in your code like so :
extension DateFormatter {
static let standardT: DateFormatter = {
var dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
return dateFormatter
}()
static let standard: DateFormatter = {
var dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter
}()
static let yearMonthDay: DateFormatter = {
var dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
return dateFormatter
}()
}
you can now just assign these to the decoder straight away by setting dateDecodingStrategyFormatters :
// Data structure
struct Dates: Codable {
var date1: Date
var date2: Date
var date3: Date
}
// The Json to decode
let jsonData = """
{
"date1": "2019-05-30 15:18:00",
"date2": "2019-05-30T05:18:00",
"date3": "2019-04-17"
}
""".data(using: .utf8)!
// Assigning mutliple DateFormatters
let decoder = JSONDecoder()
decoder.dateDecodingStrategyFormatters = [ DateFormatter.standardT,
DateFormatter.standard,
DateFormatter.yearMonthDay ]
do {
let dates = try decoder.decode(Dates.self, from: jsonData)
print(dates)
} catch let err as DecodingError {
print(err.localizedDescription)
}
Sidenotes
Once again I am aware that setting the dateDecodingStrategyFormatters as a var is a bit hacky, and I dont recommend it, you should define a function instead. However it is a personal preference to do so.

Facing this same issue, I wrote the following extension:
extension JSONDecoder.DateDecodingStrategy {
static func custom(_ formatterForKey: #escaping (CodingKey) throws -> DateFormatter?) -> JSONDecoder.DateDecodingStrategy {
return .custom({ (decoder) -> Date in
guard let codingKey = decoder.codingPath.last else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "No Coding Path Found"))
}
guard let container = try? decoder.singleValueContainer(),
let text = try? container.decode(String.self) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not decode date text"))
}
guard let dateFormatter = try formatterForKey(codingKey) else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "No date formatter for date text")
}
if let date = dateFormatter.date(from: text) {
return date
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(text)")
}
})
}
}
This extension allows you to create a DateDecodingStrategy for the JSONDecoder that handles multiple different date formats within the same JSON String. The extension contains a function that requires the implementation of a closure that gives you a CodingKey, and it is up to you to provide the correct DateFormatter for the provided key.
Lets say that you have the following JSON:
{
"publication_date": "2017-11-02",
"opening_date": "2017-11-03",
"date_updated": "2017-11-08 17:45:14"
}
The following Struct:
struct ResponseDate: Codable {
var publicationDate: Date
var openingDate: Date?
var dateUpdated: Date
enum CodingKeys: String, CodingKey {
case publicationDate = "publication_date"
case openingDate = "opening_date"
case dateUpdated = "date_updated"
}
}
Then to decode the JSON, you would use the following code:
let dateFormatterWithTime: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter
}()
let dateFormatterWithoutTime: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (key) -> DateFormatter? in
switch key {
case ResponseDate.CodingKeys.publicationDate, ResponseDate.CodingKeys.openingDate:
return dateFormatterWithoutTime
default:
return dateFormatterWithTime
}
})
let results = try? decoder.decode(ResponseDate.self, from: data)

It is a little verbose, but more flexible approach: wrap date with another Date class, and implement custom serialize methods for it. For example:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
class MyCustomDate: Codable {
var date: Date
required init?(_ date: Date?) {
if let date = date {
self.date = date
} else {
return nil
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let string = dateFormatter.string(from: date)
try container.encode(string)
}
required public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let raw = try container.decode(String.self)
if let date = dateFormatter.date(from: raw) {
self.date = date
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot parse date")
}
}
}
So now you are independent of .dateDecodingStrategy and .dateEncodingStrategy and your MyCustomDate dates will parsed with specified format. Use it in class:
class User: Codable {
var dob: MyCustomDate
}
Instantiate with
user.dob = MyCustomDate(date)

There is no way to do this with a single encoder. Your best bet here is to customize the encode(to encoder:) and init(from decoder:) methods and provide your own translation for one these values, leaving the built-in date strategy for the other one.
It might be worthwhile looking into passing one or more formatters into the userInfo object for this purpose.

I have defined this extension on DateDecodingStrategy, that takes an array of date formatters as an argument:
extension JSONDecoder.DateDecodingStrategy {
static func anyFormatter(in formatters: [DateFormatter]) -> Self {
return .custom { decoder in
guard formatters.count > 0 else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "No date formatter provided"))
}
guard let dateString = try? decoder.singleValueContainer().decode(String.self) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not decode date string"))
}
let successfullyFormattedDates = formatters.lazy.compactMap { $0.date(from: dateString) }
guard let date = successfullyFormattedDates.first else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Date string \"\(dateString)\" does not match any of the expected formats (\(formatters.compactMap(\.dateFormat).joined(separator: " or ")))"))
}
return date
}
}
}
and I use it this way:
let format1 = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
let format2 = DateFormatter(format: "yyyy-MM-dd'T'HH:mmzzzzzz")
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .anyFormatter(in: [. format1, . format2])
It successively tries to convert the date using the different formatters provided (lazily, so that it stops after the first successful attempt), and if every format fails then it throws an error.

If you have multiple dates with different formats in single model, its bit difficult to apply .dateDecodingStrategy for each dates.
Check here https://gist.github.com/romanroibu/089ec641757604bf78a390654c437cb0 for a handy solution

Add an extension to KeyedDecodingContainer
extension KeyedDecodingContainer {
func decodeDate(forKey key: KeyedDecodingContainer<K>.Key, withPossible formats: [DateFormatter]) throws -> Date? {
for format in formats {
if let date = format.date(from: try self.decode(String.self, forKey: key)) {
return date
}
}
throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "Date string does not match format expected by formatter.")
}
}
and use 'try container.decodeDate(forKey: 'key', withPossible: [.iso8601Full, .yyyyMMdd])'
full solution is here:
import Foundation
extension DateFormatter {
static let iso8601Full: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.calendar = Calendar(identifier: .iso8601)
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()
static let yyyyMMdd: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.calendar = Calendar(identifier: .iso8601)
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()
}
public struct RSSFeed: Codable {
public let releaseDate: Date?
public let releaseDateAndTime: Date?
}
extension RSSFeed {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
releaseDate = try container.decodeDate(forKey: .releaseDate, withPossible: [.iso8601Full, .yyyyMMdd])
releaseDateAndTime = try container.decodeDate(forKey: .releaseDateAndTime, withPossible: [.iso8601Full, .yyyyMMdd])
}
}
extension KeyedDecodingContainer {
func decodeDate(forKey key: KeyedDecodingContainer<K>.Key, withPossible formats: [DateFormatter]) throws -> Date? {
for format in formats {
if let date = format.date(from: try self.decode(String.self, forKey: key)) {
return date
}
}
throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "Date string does not match format expected by formatter.")
}
}
let json = """
{
"releaseDate":"2017-11-12",
"releaseDateAndTime":"2017-11-16 02:02:55"
}
"""
let data = Data(json.utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full)
let rssFeed = try! decoder.decode(RSSFeed.self, from: data)
let feed = rssFeed
print(feed.releaseDate, feed.releaseDateAndTime)

Related

Swift String to Date conversion from youtube API publishedAt [duplicate]

How to generate a date time stamp, using the format standards for ISO 8601 and RFC 3339?
The goal is a string that looks like this:
"2015-01-01T00:00:00.000Z"
Format:
year, month, day, as "XXXX-XX-XX"
the letter "T" as a separator
hour, minute, seconds, milliseconds, as "XX:XX:XX.XXX".
the letter "Z" as a zone designator for zero offset, a.k.a. UTC, GMT, Zulu time.
Best case:
Swift source code that is simple, short, and straightforward.
No need to use any additional framework, subproject, cocoapod, C code, etc.
I've searched StackOverflow, Google, Apple, etc. and haven't found a Swift answer to this.
The classes that seem most promising are NSDate, NSDateFormatter, NSTimeZone.
Related Q&A: How do I get an ISO 8601 date on iOS?
Here's the best I've come up with so far:
var now = NSDate()
var formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
println(formatter.stringFromDate(now))
Swift 4 • iOS 11.2.1 or later
extension ISO8601DateFormatter {
convenience init(_ formatOptions: Options) {
self.init()
self.formatOptions = formatOptions
}
}
extension Formatter {
static let iso8601withFractionalSeconds = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds])
}
extension Date {
var iso8601withFractionalSeconds: String { return Formatter.iso8601withFractionalSeconds.string(from: self) }
}
extension String {
var iso8601withFractionalSeconds: Date? { return Formatter.iso8601withFractionalSeconds.date(from: self) }
}
Usage:
Date().description(with: .current) // Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"
let dateString = Date().iso8601withFractionalSeconds // "2019-02-06T00:35:01.746Z"
if let date = dateString.iso8601withFractionalSeconds {
date.description(with: .current) // "Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"
print(date.iso8601withFractionalSeconds) // "2019-02-06T00:35:01.746Z\n"
}
iOS 9 • Swift 3 or later
extension Formatter {
static let iso8601withFractionalSeconds: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
return formatter
}()
}
Codable Protocol
If you need to encode and decode this format when working with Codable
protocol you can create your own custom date encoding/decoding strategies:
extension JSONDecoder.DateDecodingStrategy {
static let iso8601withFractionalSeconds = custom {
let container = try $0.singleValueContainer()
let string = try container.decode(String.self)
guard let date = Formatter.iso8601withFractionalSeconds.date(from: string) else {
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Invalid date: " + string)
}
return date
}
}
and the encoding strategy
extension JSONEncoder.DateEncodingStrategy {
static let iso8601withFractionalSeconds = custom {
var container = $1.singleValueContainer()
try container.encode(Formatter.iso8601withFractionalSeconds.string(from: $0))
}
}
Playground Testing
let dates = [Date()] // ["Feb 8, 2019 at 9:48 PM"]
encoding
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601withFractionalSeconds
let data = try! encoder.encode(dates)
print(String(data: data, encoding: .utf8)!)
decoding
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
let decodedDates = try! decoder.decode([Date].self, from: data) // ["Feb 8, 2019 at 9:48 PM"]
Remember to set the locale to en_US_POSIX as described in Technical Q&A1480. In Swift 3:
let date = Date()
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
print(formatter.string(from: date))
The issue is that if you're on a device which is using a non-Gregorian calendar, the year will not conform to RFC3339/ISO8601 unless you specify the locale as well as the timeZone and dateFormat string.
Or you can use ISO8601DateFormatter to get you out of the weeds of setting locale and timeZone yourself:
let date = Date()
let formatter = ISO8601DateFormatter()
formatter.formatOptions.insert(.withFractionalSeconds) // this is only available effective iOS 11 and macOS 10.13
print(formatter.string(from: date))
For Swift 2 rendition, see previous revision of this answer.
If you want to use the ISO8601DateFormatter() with a date from a Rails 4+ JSON feed (and don't need millis of course), you need to set a few options on the formatter for it to work right otherwise the the date(from: string) function will return nil. Here's what I'm using:
extension Date {
init(dateString:String) {
self = Date.iso8601Formatter.date(from: dateString)!
}
static let iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate,
.withTime,
.withDashSeparatorInDate,
.withColonSeparatorInTime]
return formatter
}()
}
Here's the result of using the options versus not in a playground screenshot:
Swift 5
If you're targeting iOS 11.0+ / macOS 10.13+, you simply use ISO8601DateFormatter with the withInternetDateTime and withFractionalSeconds options, like so:
let date = Date()
let iso8601DateFormatter = ISO8601DateFormatter()
iso8601DateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let string = iso8601DateFormatter.string(from: date)
// string looks like "2020-03-04T21:39:02.112Z"
To further compliment Andrés Torres Marroquín and Leo Dabus, I have a version that preserves fractional seconds. I can't find it documented anywhere, but Apple truncate fractional seconds to the microsecond (3 digits of precision) on both input and output (even though specified using SSSSSSS, contrary to Unicode tr35-31).
I should stress that this is probably not necessary for most use cases. Dates online do not typically need millisecond precision, and when they do, it is often better to use a different data format. But sometimes one must interoperate with a pre-existing system in a particular way.
Xcode 8/9 and Swift 3.0-3.2
extension Date {
struct Formatter {
static let iso8601: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(identifier: "UTC")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXXXX"
return formatter
}()
}
var iso8601: String {
// create base Date format
var formatted = DateFormatter.iso8601.string(from: self)
// Apple returns millisecond precision. find the range of the decimal portion
if let fractionStart = formatted.range(of: "."),
let fractionEnd = formatted.index(fractionStart.lowerBound, offsetBy: 7, limitedBy: formatted.endIndex) {
let fractionRange = fractionStart.lowerBound..<fractionEnd
// replace the decimal range with our own 6 digit fraction output
let microseconds = self.timeIntervalSince1970 - floor(self.timeIntervalSince1970)
var microsecondsStr = String(format: "%.06f", microseconds)
microsecondsStr.remove(at: microsecondsStr.startIndex)
formatted.replaceSubrange(fractionRange, with: microsecondsStr)
}
return formatted
}
}
extension String {
var dateFromISO8601: Date? {
guard let parsedDate = Date.Formatter.iso8601.date(from: self) else {
return nil
}
var preliminaryDate = Date(timeIntervalSinceReferenceDate: floor(parsedDate.timeIntervalSinceReferenceDate))
if let fractionStart = self.range(of: "."),
let fractionEnd = self.index(fractionStart.lowerBound, offsetBy: 7, limitedBy: self.endIndex) {
let fractionRange = fractionStart.lowerBound..<fractionEnd
let fractionStr = self.substring(with: fractionRange)
if var fraction = Double(fractionStr) {
fraction = Double(floor(1000000*fraction)/1000000)
preliminaryDate.addTimeInterval(fraction)
}
}
return preliminaryDate
}
}
Uses ISO8601DateFormatter on iOS10 or newer.
Uses DateFormatter on iOS9 or older.
Swift 4
protocol DateFormatterProtocol {
func string(from date: Date) -> String
func date(from string: String) -> Date?
}
extension DateFormatter: DateFormatterProtocol {}
#available(iOS 10.0, *)
extension ISO8601DateFormatter: DateFormatterProtocol {}
struct DateFormatterShared {
static let iso8601: DateFormatterProtocol = {
if #available(iOS 10, *) {
return ISO8601DateFormatter()
} else {
// iOS 9
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
return formatter
}
}()
}
In my case I have to convert the DynamoDB - lastUpdated column (Unix Timestamp) to Normal Time.
The initial value of lastUpdated was : 1460650607601 - converted down to 2016-04-14 16:16:47 +0000 via :
if let lastUpdated : String = userObject.lastUpdated {
let epocTime = NSTimeInterval(lastUpdated)! / 1000 // convert it from milliseconds dividing it by 1000
let unixTimestamp = NSDate(timeIntervalSince1970: epocTime) //convert unix timestamp to Date
let dateFormatter = NSDateFormatter()
dateFormatter.timeZone = NSTimeZone()
dateFormatter.locale = NSLocale.currentLocale() // NSLocale(localeIdentifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
dateFormatter.dateFromString(String(unixTimestamp))
let updatedTimeStamp = unixTimestamp
print(updatedTimeStamp)
}
In the future the format might need to be changed which could be a small head ache having date.dateFromISO8601 calls everywhere in an app. Use a class and protocol to wrap the implementation, changing the date time format call in one place will be simpler. Use RFC3339 if possible, its a more complete representation. DateFormatProtocol and DateFormat is great for dependency injection.
class AppDelegate: UIResponder, UIApplicationDelegate {
internal static let rfc3339DateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
internal static let localeEnUsPosix = "en_US_POSIX"
}
import Foundation
protocol DateFormatProtocol {
func format(date: NSDate) -> String
func parse(date: String) -> NSDate?
}
import Foundation
class DateFormat: DateFormatProtocol {
func format(date: NSDate) -> String {
return date.rfc3339
}
func parse(date: String) -> NSDate? {
return date.rfc3339
}
}
extension NSDate {
struct Formatter {
static let rfc3339: NSDateFormatter = {
let formatter = NSDateFormatter()
formatter.calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierISO8601)
formatter.locale = NSLocale(localeIdentifier: AppDelegate.localeEnUsPosix)
formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
formatter.dateFormat = rfc3339DateFormat
return formatter
}()
}
var rfc3339: String { return Formatter.rfc3339.stringFromDate(self) }
}
extension String {
var rfc3339: NSDate? {
return NSDate.Formatter.rfc3339.dateFromString(self)
}
}
class DependencyService: DependencyServiceProtocol {
private var dateFormat: DateFormatProtocol?
func setDateFormat(dateFormat: DateFormatProtocol) {
self.dateFormat = dateFormat
}
func getDateFormat() -> DateFormatProtocol {
if let dateFormatObject = dateFormat {
return dateFormatObject
} else {
let dateFormatObject = DateFormat()
dateFormat = dateFormatObject
return dateFormatObject
}
}
}
There is a new ISO8601DateFormatter class that let's you create a string with just one line. For backwards compatibility I used an old C-library. I hope this is useful for someone.
Swift 3.0
extension Date {
var iso8601: String {
if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
return ISO8601DateFormatter.string(from: self, timeZone: TimeZone.current, formatOptions: .withInternetDateTime)
} else {
var buffer = [CChar](repeating: 0, count: 25)
var time = time_t(self.timeIntervalSince1970)
strftime_l(&buffer, buffer.count, "%FT%T%z", localtime(&time), nil)
return String(cString: buffer)
}
}
}
To complement the version of Leo Dabus, I added support for projects written Swift and Objective-C, also added support for the optional milliseconds, probably isn't the best but you would get the point:
Xcode 8 and Swift 3
extension Date {
struct Formatter {
static let iso8601: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
return formatter
}()
}
var iso8601: String {
return Formatter.iso8601.string(from: self)
}
}
extension String {
var dateFromISO8601: Date? {
var data = self
if self.range(of: ".") == nil {
// Case where the string doesn't contain the optional milliseconds
data = data.replacingOccurrences(of: "Z", with: ".000000Z")
}
return Date.Formatter.iso8601.date(from: data)
}
}
extension NSString {
var dateFromISO8601: Date? {
return (self as String).dateFromISO8601
}
}
It is now 2022, but I was looking for an answer to this (i.e. how to convert a Date to ISO8601 that includes fractions of seconds). It turns out the answer nowadays is a one-liner:
var somedate: Date = Date.now
var isodate = somedate.ISO8601Format(Date.ISO8601FormatStyle(includingFractionalSeconds: true))
so this will print something like 2022-08-16T17:45:08.548Z
Without some manual String masks or TimeFormatters
import Foundation
struct DateISO: Codable {
var date: Date
}
extension Date{
var isoString: String {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
guard let data = try? encoder.encode(DateISO(date: self)),
let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: String]
else { return "" }
return json?.first?.value ?? ""
}
}
let dateString = Date().isoString
Based on the acceptable answer in an object paradigm
class ISO8601Format
{
let format: ISO8601DateFormatter
init() {
let format = ISO8601DateFormatter()
format.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
format.timeZone = TimeZone(secondsFromGMT: 0)!
self.format = format
}
func date(from string: String) -> Date {
guard let date = format.date(from: string) else { fatalError() }
return date
}
func string(from date: Date) -> String { return format.string(from: date) }
}
class ISO8601Time
{
let date: Date
let format = ISO8601Format() //FIXME: Duplication
required init(date: Date) { self.date = date }
convenience init(string: String) {
let format = ISO8601Format() //FIXME: Duplication
let date = format.date(from: string)
self.init(date: date)
}
func concise() -> String { return format.string(from: date) }
func description() -> String { return date.description(with: .current) }
}
callsite
let now = Date()
let time1 = ISO8601Time(date: now)
print("time1.concise(): \(time1.concise())")
print("time1: \(time1.description())")
let time2 = ISO8601Time(string: "2020-03-24T23:16:17.661Z")
print("time2.concise(): \(time2.concise())")
print("time2: \(time2.description())")

Couldn't convert date string from server to Date type using dateformatter [duplicate]

How to generate a date time stamp, using the format standards for ISO 8601 and RFC 3339?
The goal is a string that looks like this:
"2015-01-01T00:00:00.000Z"
Format:
year, month, day, as "XXXX-XX-XX"
the letter "T" as a separator
hour, minute, seconds, milliseconds, as "XX:XX:XX.XXX".
the letter "Z" as a zone designator for zero offset, a.k.a. UTC, GMT, Zulu time.
Best case:
Swift source code that is simple, short, and straightforward.
No need to use any additional framework, subproject, cocoapod, C code, etc.
I've searched StackOverflow, Google, Apple, etc. and haven't found a Swift answer to this.
The classes that seem most promising are NSDate, NSDateFormatter, NSTimeZone.
Related Q&A: How do I get an ISO 8601 date on iOS?
Here's the best I've come up with so far:
var now = NSDate()
var formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
println(formatter.stringFromDate(now))
Swift 4 • iOS 11.2.1 or later
extension ISO8601DateFormatter {
convenience init(_ formatOptions: Options) {
self.init()
self.formatOptions = formatOptions
}
}
extension Formatter {
static let iso8601withFractionalSeconds = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds])
}
extension Date {
var iso8601withFractionalSeconds: String { return Formatter.iso8601withFractionalSeconds.string(from: self) }
}
extension String {
var iso8601withFractionalSeconds: Date? { return Formatter.iso8601withFractionalSeconds.date(from: self) }
}
Usage:
Date().description(with: .current) // Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"
let dateString = Date().iso8601withFractionalSeconds // "2019-02-06T00:35:01.746Z"
if let date = dateString.iso8601withFractionalSeconds {
date.description(with: .current) // "Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"
print(date.iso8601withFractionalSeconds) // "2019-02-06T00:35:01.746Z\n"
}
iOS 9 • Swift 3 or later
extension Formatter {
static let iso8601withFractionalSeconds: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
return formatter
}()
}
Codable Protocol
If you need to encode and decode this format when working with Codable
protocol you can create your own custom date encoding/decoding strategies:
extension JSONDecoder.DateDecodingStrategy {
static let iso8601withFractionalSeconds = custom {
let container = try $0.singleValueContainer()
let string = try container.decode(String.self)
guard let date = Formatter.iso8601withFractionalSeconds.date(from: string) else {
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Invalid date: " + string)
}
return date
}
}
and the encoding strategy
extension JSONEncoder.DateEncodingStrategy {
static let iso8601withFractionalSeconds = custom {
var container = $1.singleValueContainer()
try container.encode(Formatter.iso8601withFractionalSeconds.string(from: $0))
}
}
Playground Testing
let dates = [Date()] // ["Feb 8, 2019 at 9:48 PM"]
encoding
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601withFractionalSeconds
let data = try! encoder.encode(dates)
print(String(data: data, encoding: .utf8)!)
decoding
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
let decodedDates = try! decoder.decode([Date].self, from: data) // ["Feb 8, 2019 at 9:48 PM"]
Remember to set the locale to en_US_POSIX as described in Technical Q&A1480. In Swift 3:
let date = Date()
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
print(formatter.string(from: date))
The issue is that if you're on a device which is using a non-Gregorian calendar, the year will not conform to RFC3339/ISO8601 unless you specify the locale as well as the timeZone and dateFormat string.
Or you can use ISO8601DateFormatter to get you out of the weeds of setting locale and timeZone yourself:
let date = Date()
let formatter = ISO8601DateFormatter()
formatter.formatOptions.insert(.withFractionalSeconds) // this is only available effective iOS 11 and macOS 10.13
print(formatter.string(from: date))
For Swift 2 rendition, see previous revision of this answer.
If you want to use the ISO8601DateFormatter() with a date from a Rails 4+ JSON feed (and don't need millis of course), you need to set a few options on the formatter for it to work right otherwise the the date(from: string) function will return nil. Here's what I'm using:
extension Date {
init(dateString:String) {
self = Date.iso8601Formatter.date(from: dateString)!
}
static let iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate,
.withTime,
.withDashSeparatorInDate,
.withColonSeparatorInTime]
return formatter
}()
}
Here's the result of using the options versus not in a playground screenshot:
Swift 5
If you're targeting iOS 11.0+ / macOS 10.13+, you simply use ISO8601DateFormatter with the withInternetDateTime and withFractionalSeconds options, like so:
let date = Date()
let iso8601DateFormatter = ISO8601DateFormatter()
iso8601DateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let string = iso8601DateFormatter.string(from: date)
// string looks like "2020-03-04T21:39:02.112Z"
To further compliment Andrés Torres Marroquín and Leo Dabus, I have a version that preserves fractional seconds. I can't find it documented anywhere, but Apple truncate fractional seconds to the microsecond (3 digits of precision) on both input and output (even though specified using SSSSSSS, contrary to Unicode tr35-31).
I should stress that this is probably not necessary for most use cases. Dates online do not typically need millisecond precision, and when they do, it is often better to use a different data format. But sometimes one must interoperate with a pre-existing system in a particular way.
Xcode 8/9 and Swift 3.0-3.2
extension Date {
struct Formatter {
static let iso8601: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(identifier: "UTC")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXXXX"
return formatter
}()
}
var iso8601: String {
// create base Date format
var formatted = DateFormatter.iso8601.string(from: self)
// Apple returns millisecond precision. find the range of the decimal portion
if let fractionStart = formatted.range(of: "."),
let fractionEnd = formatted.index(fractionStart.lowerBound, offsetBy: 7, limitedBy: formatted.endIndex) {
let fractionRange = fractionStart.lowerBound..<fractionEnd
// replace the decimal range with our own 6 digit fraction output
let microseconds = self.timeIntervalSince1970 - floor(self.timeIntervalSince1970)
var microsecondsStr = String(format: "%.06f", microseconds)
microsecondsStr.remove(at: microsecondsStr.startIndex)
formatted.replaceSubrange(fractionRange, with: microsecondsStr)
}
return formatted
}
}
extension String {
var dateFromISO8601: Date? {
guard let parsedDate = Date.Formatter.iso8601.date(from: self) else {
return nil
}
var preliminaryDate = Date(timeIntervalSinceReferenceDate: floor(parsedDate.timeIntervalSinceReferenceDate))
if let fractionStart = self.range(of: "."),
let fractionEnd = self.index(fractionStart.lowerBound, offsetBy: 7, limitedBy: self.endIndex) {
let fractionRange = fractionStart.lowerBound..<fractionEnd
let fractionStr = self.substring(with: fractionRange)
if var fraction = Double(fractionStr) {
fraction = Double(floor(1000000*fraction)/1000000)
preliminaryDate.addTimeInterval(fraction)
}
}
return preliminaryDate
}
}
Uses ISO8601DateFormatter on iOS10 or newer.
Uses DateFormatter on iOS9 or older.
Swift 4
protocol DateFormatterProtocol {
func string(from date: Date) -> String
func date(from string: String) -> Date?
}
extension DateFormatter: DateFormatterProtocol {}
#available(iOS 10.0, *)
extension ISO8601DateFormatter: DateFormatterProtocol {}
struct DateFormatterShared {
static let iso8601: DateFormatterProtocol = {
if #available(iOS 10, *) {
return ISO8601DateFormatter()
} else {
// iOS 9
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
return formatter
}
}()
}
In my case I have to convert the DynamoDB - lastUpdated column (Unix Timestamp) to Normal Time.
The initial value of lastUpdated was : 1460650607601 - converted down to 2016-04-14 16:16:47 +0000 via :
if let lastUpdated : String = userObject.lastUpdated {
let epocTime = NSTimeInterval(lastUpdated)! / 1000 // convert it from milliseconds dividing it by 1000
let unixTimestamp = NSDate(timeIntervalSince1970: epocTime) //convert unix timestamp to Date
let dateFormatter = NSDateFormatter()
dateFormatter.timeZone = NSTimeZone()
dateFormatter.locale = NSLocale.currentLocale() // NSLocale(localeIdentifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
dateFormatter.dateFromString(String(unixTimestamp))
let updatedTimeStamp = unixTimestamp
print(updatedTimeStamp)
}
In the future the format might need to be changed which could be a small head ache having date.dateFromISO8601 calls everywhere in an app. Use a class and protocol to wrap the implementation, changing the date time format call in one place will be simpler. Use RFC3339 if possible, its a more complete representation. DateFormatProtocol and DateFormat is great for dependency injection.
class AppDelegate: UIResponder, UIApplicationDelegate {
internal static let rfc3339DateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
internal static let localeEnUsPosix = "en_US_POSIX"
}
import Foundation
protocol DateFormatProtocol {
func format(date: NSDate) -> String
func parse(date: String) -> NSDate?
}
import Foundation
class DateFormat: DateFormatProtocol {
func format(date: NSDate) -> String {
return date.rfc3339
}
func parse(date: String) -> NSDate? {
return date.rfc3339
}
}
extension NSDate {
struct Formatter {
static let rfc3339: NSDateFormatter = {
let formatter = NSDateFormatter()
formatter.calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierISO8601)
formatter.locale = NSLocale(localeIdentifier: AppDelegate.localeEnUsPosix)
formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
formatter.dateFormat = rfc3339DateFormat
return formatter
}()
}
var rfc3339: String { return Formatter.rfc3339.stringFromDate(self) }
}
extension String {
var rfc3339: NSDate? {
return NSDate.Formatter.rfc3339.dateFromString(self)
}
}
class DependencyService: DependencyServiceProtocol {
private var dateFormat: DateFormatProtocol?
func setDateFormat(dateFormat: DateFormatProtocol) {
self.dateFormat = dateFormat
}
func getDateFormat() -> DateFormatProtocol {
if let dateFormatObject = dateFormat {
return dateFormatObject
} else {
let dateFormatObject = DateFormat()
dateFormat = dateFormatObject
return dateFormatObject
}
}
}
There is a new ISO8601DateFormatter class that let's you create a string with just one line. For backwards compatibility I used an old C-library. I hope this is useful for someone.
Swift 3.0
extension Date {
var iso8601: String {
if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
return ISO8601DateFormatter.string(from: self, timeZone: TimeZone.current, formatOptions: .withInternetDateTime)
} else {
var buffer = [CChar](repeating: 0, count: 25)
var time = time_t(self.timeIntervalSince1970)
strftime_l(&buffer, buffer.count, "%FT%T%z", localtime(&time), nil)
return String(cString: buffer)
}
}
}
To complement the version of Leo Dabus, I added support for projects written Swift and Objective-C, also added support for the optional milliseconds, probably isn't the best but you would get the point:
Xcode 8 and Swift 3
extension Date {
struct Formatter {
static let iso8601: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
return formatter
}()
}
var iso8601: String {
return Formatter.iso8601.string(from: self)
}
}
extension String {
var dateFromISO8601: Date? {
var data = self
if self.range(of: ".") == nil {
// Case where the string doesn't contain the optional milliseconds
data = data.replacingOccurrences(of: "Z", with: ".000000Z")
}
return Date.Formatter.iso8601.date(from: data)
}
}
extension NSString {
var dateFromISO8601: Date? {
return (self as String).dateFromISO8601
}
}
It is now 2022, but I was looking for an answer to this (i.e. how to convert a Date to ISO8601 that includes fractions of seconds). It turns out the answer nowadays is a one-liner:
var somedate: Date = Date.now
var isodate = somedate.ISO8601Format(Date.ISO8601FormatStyle(includingFractionalSeconds: true))
so this will print something like 2022-08-16T17:45:08.548Z
Without some manual String masks or TimeFormatters
import Foundation
struct DateISO: Codable {
var date: Date
}
extension Date{
var isoString: String {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
guard let data = try? encoder.encode(DateISO(date: self)),
let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: String]
else { return "" }
return json?.first?.value ?? ""
}
}
let dateString = Date().isoString
Based on the acceptable answer in an object paradigm
class ISO8601Format
{
let format: ISO8601DateFormatter
init() {
let format = ISO8601DateFormatter()
format.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
format.timeZone = TimeZone(secondsFromGMT: 0)!
self.format = format
}
func date(from string: String) -> Date {
guard let date = format.date(from: string) else { fatalError() }
return date
}
func string(from date: Date) -> String { return format.string(from: date) }
}
class ISO8601Time
{
let date: Date
let format = ISO8601Format() //FIXME: Duplication
required init(date: Date) { self.date = date }
convenience init(string: String) {
let format = ISO8601Format() //FIXME: Duplication
let date = format.date(from: string)
self.init(date: date)
}
func concise() -> String { return format.string(from: date) }
func description() -> String { return date.description(with: .current) }
}
callsite
let now = Date()
let time1 = ISO8601Time(date: now)
print("time1.concise(): \(time1.concise())")
print("time1: \(time1.description())")
let time2 = ISO8601Time(string: "2020-03-24T23:16:17.661Z")
print("time2.concise(): \(time2.concise())")
print("time2: \(time2.description())")

Saving an object with date to user defaults - date won't be the same

I am saving and restoring a simple struct that contains a date. The reason for using this string format is that RESTful API is returning this date format. Meaning that it shouldn't be changed for this code example below.
extension DateFormatter {
static let yyyyMMdd: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
}
struct Person: Codable, Equatable {
let birthDate: Date
private enum CodingKeys: String, CodingKey {
case birthDate
}
}
func storePerson(_ person: Person) {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(DateFormatter.yyyyMMdd)
guard let encodedPerson = try? encoder.encode(person) else {
return
}
UserDefaults.standard.set(encodedPerson, forKey: "savedPerson")
}
func fetchPerson() -> Person {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.yyyyMMdd)
guard let savedPerson = UserDefaults.standard.data(forKey: "savedPerson"),
let person = try? decoder.decode(Person.self, from: savedPerson) else {
fatalError()
}
return person
}
let person = Person(birthDate: Date())
storePerson(person)
let storedPerson = fetchPerson()
print("\(person == storedPerson)") // this will be false
print("\(person.birthDate)")
print("\(storedPerson.birthDate)") // same date as above, but with no minutes
Why is the date not the same? The encoder and decoder are using the same dateEncodingStrategy

Group Items By Property in Swift 4

My data source is
{"events": [{"name":"event
foo","date":"2018-07-21","time":"7:00","am_or_pm":"PM","day":"Saturday","description":"test
"}, {"name":"event
bar","date":"2018-07-21","time":"7:00","am_or_pm":"PM","day":"Saturday","description":"test2"},
{"name":"event
foobar","date":"2018-07-21","time":"11:00","am_or_pm":"PM","day":"Saturday","description":"test3"}]}
I have tried dictionary/arrays, but not really getting close to my wanted result.
Pulling out data into an array:
var times = ["9:00","9:00","11:00"]
var names = ["event foo","event bar","event foobar"]
Desired output:
["9:00", "11:00"]
[["event foo", "event bar"], ["event foobar"]]
Any pointers to do this in Swift is appreciated. My end result is to hope to section a uitableview grouped by time.
If using Swift 4, you can use reduce(into:) and the default value for subscript operator:
guard
let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: [Any]],
let events = json["events"] as? [[String: String]] else {
return
}
let results = events.reduce(into: [String: [String]]()) { result, value in
guard let time = value["time"], let name = value["name"] else { return }
result[time, default: []].append(name)
}
That results in a dictionary:
["11:00": ["event foobar"], "7:00": ["event foo", "event bar"]]
Or, as Vadian suggested, you can use Dictionary(grouping:,by:), but then you have to map it if you only want your name values, resulting in an array of tuples:
let results = Dictionary(grouping: events, by: { $0["time"]! })
.map { ($0.key, $0.value.map { $0["name"]! })}
[("11:00", ["event foobar"]), ("7:00", ["event foo", "event bar"])]
Personally, like Vadian suggested, I'd be inclined to combine date, time, and am_or_pm to build a full Date object and use one of the above patterns. E.g.:
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd h:mm a"
formatter.locale = Locale(identifier: "en_US_POSIX")
// if date/time values are in GMT, uncomment the following line:
//
// formatter.timeZone = TimeZone(secondsFromGMT: 0)
let results = events.reduce(into: [Date: [String]]()) { result, value in
guard
let timeString = value["time"],
let dateString = value["date"],
let amPm = value["am_or_pm"],
let date = formatter.date(from: dateString + " " + timeString + " " + amPm),
let name = value["name"] else { return }
result[date, default: []].append(name)
}
or
let results = Dictionary(grouping: events, by: { dictionary -> Date in
let string = dictionary["date"]! + " " + dictionary["time"]! + " " + dictionary["am_or_pm"]!
return formatter.date(from: string)!
})
.map { ($0.key, $0.value.map { $0["name"]! })}
Or, if the web service returned a single ISO 8601/RFC 3339 string representation of the date, time, and am/pm in the JSON, this could be simplified further.
I recommend to decode the JSON with Decodable and create a full date from the components. Then use Dictionary(grouping:by:) to group the array.
First create a DateFormatter (uncomment the time zone line if you need absolute UTC dates)
let dateFormatter : DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
// formatter.timeZone = TimeZone(secondsFromGMT: 0)!
formatter.dateFormat = "yyyy-MM-dd hh:mm a"
return formatter
}()
Create two structs for the root element and the events array. A custom initializer creates the Date instance
struct Root : Decodable {
let events : [Event]
}
struct Event : Decodable {
let date : Date
let name, description : String
private enum CodingKeys: String, CodingKey { case name, date, time, am_or_pm, description}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
description = try container.decode(String.self, forKey: .description)
let datePortion = try container.decode(String.self, forKey: .date)
let timePortion = try container.decode(String.self, forKey: .time)
let ampm = try container.decode(String.self, forKey: .am_or_pm)
let dateString = "\(datePortion) \(timePortion) \(ampm)"
guard let fullDate = dateFormatter.date(from: dateString) else {
throw DecodingError.dataCorruptedError(forKey: .date,
in: container,
debugDescription: "Date cannot be created")
}
date = fullDate
}
}
Decode the JSON and group the array
let jsonString = """
{"events": [{"name":"event foo","date":"2018-07-21","time":"7:00","am_or_pm":"PM","day":"Saturday","description":"test "}, {"name":"event bar","date":"2018-07-21","time":"7:00","am_or_pm":"PM","day":"Saturday","description":"test2"}, {"name":"event foobar","date":"2018-07-21","time":"11:00","am_or_pm":"PM","day":"Saturday","description":"test3"}]}
"""
do {
let data = Data(jsonString.utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(Root.self, from: data)
let grouped = Dictionary(grouping: result.events, by: { $0.date})
print(grouped)
} catch {
print("error: ", error)
}

Data Corrupted ISO8601 Decodable Error Swift 4 [duplicate]

I am replacing my old JSON parsing code with Swift's Codable and am running into a bit of a snag. I guess it isn't as much a Codable question as it is a DateFormatter question.
Start with a struct
struct JustADate: Codable {
var date: Date
}
and a json string
let json = """
{ "date": "2017-06-19T18:43:19Z" }
"""
now lets decode
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let data = json.data(using: .utf8)!
let justADate = try! decoder.decode(JustADate.self, from: data) //all good
But if we change the date so that it has fractional seconds, for example:
let json = """
{ "date": "2017-06-19T18:43:19.532Z" }
"""
Now it breaks. The dates sometimes come back with fractional seconds and sometimes do not. The way I used to solve it was in my mapping code I had a transform function that tried both dateFormats with and without the fractional seconds. I am not quite sure how to approach it using Codable however. Any suggestions?
You can use two different date formatters (with and without fraction seconds) and create a custom DateDecodingStrategy. In case of failure when parsing the date returned by the API you can throw a DecodingError as suggested by #PauloMattos in comments:
iOS 9, macOS 10.9, tvOS 9, watchOS 2, Xcode 9 or later
The custom ISO8601 DateFormatter:
extension Formatter {
static let iso8601withFractionalSeconds: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
return formatter
}()
static let iso8601: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX"
return formatter
}()
}
The custom DateDecodingStrategy:
extension JSONDecoder.DateDecodingStrategy {
static let customISO8601 = custom {
let container = try $0.singleValueContainer()
let string = try container.decode(String.self)
if let date = Formatter.iso8601withFractionalSeconds.date(from: string) ?? Formatter.iso8601.date(from: string) {
return date
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
}
}
The custom DateEncodingStrategy:
extension JSONEncoder.DateEncodingStrategy {
static let customISO8601 = custom {
var container = $1.singleValueContainer()
try container.encode(Formatter.iso8601withFractionalSeconds.string(from: $0))
}
}
edit/update:
Xcode 10 • Swift 4.2 or later • iOS 11.2.1 or later
ISO8601DateFormatter now supports formatOptions .withFractionalSeconds:
extension Formatter {
static let iso8601withFractionalSeconds: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}()
static let iso8601: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]
return formatter
}()
}
The customs DateDecodingStrategy and DateEncodingStrategy would be the same as shown above.
// Playground testing
struct ISODates: Codable {
let dateWith9FS: Date
let dateWith3FS: Date
let dateWith2FS: Date
let dateWithoutFS: Date
}
let isoDatesJSON = """
{
"dateWith9FS": "2017-06-19T18:43:19.532123456Z",
"dateWith3FS": "2017-06-19T18:43:19.532Z",
"dateWith2FS": "2017-06-19T18:43:19.53Z",
"dateWithoutFS": "2017-06-19T18:43:19Z",
}
"""
let isoDatesData = Data(isoDatesJSON.utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .customISO8601
do {
let isoDates = try decoder.decode(ISODates.self, from: isoDatesData)
print(Formatter.iso8601withFractionalSeconds.string(from: isoDates.dateWith9FS)) // 2017-06-19T18:43:19.532Z
print(Formatter.iso8601withFractionalSeconds.string(from: isoDates.dateWith3FS)) // 2017-06-19T18:43:19.532Z
print(Formatter.iso8601withFractionalSeconds.string(from: isoDates.dateWith2FS)) // 2017-06-19T18:43:19.530Z
print(Formatter.iso8601withFractionalSeconds.string(from: isoDates.dateWithoutFS)) // 2017-06-19T18:43:19.000Z
} catch {
print(error)
}
Swift 5
To parse ISO8601 string to date you have to use DateFormatter. In newer systems (f.ex. iOS11+) you can use ISO8601DateFormatter.
As long as you don't know if date contains milliseconds, you should create 2 formatters for each case. Then, during parsing String to Date use both consequently.
DateFormatter for older systems
/// Formatter for ISO8601 with milliseconds
lazy var iso8601FormatterWithMilliseconds: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(abbreviation: "GMT")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
return dateFormatter
}()
/// Formatter for ISO8601 without milliseconds
lazy var iso8601Formatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(abbreviation: "GMT")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
return dateFormatter
}()
ISO8601DateFormatter for newer systems (f.ex. iOS 11+)
lazy var iso8601FormatterWithMilliseconds: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
// GMT or UTC -> UTC is standard, GMT is TimeZone
formatter.timeZone = TimeZone(abbreviation: "GMT")
formatter.formatOptions = [.withInternetDateTime,
.withDashSeparatorInDate,
.withColonSeparatorInTime,
.withTimeZone,
.withFractionalSeconds]
return formatter
}()
/// Formatter for ISO8601 without milliseconds
lazy var iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
// GMT or UTC -> UTC is standard, GMT is TimeZone
formatter.timeZone = TimeZone(abbreviation: "GMT")
formatter.formatOptions = [.withInternetDateTime,
.withDashSeparatorInDate,
.withColonSeparatorInTime,
.withTimeZone]
return formatter
}()
Summary
As you can notice there is 2 formatters to create. If you want to support older systems, it makes 4 formatters. To make it more simple, check out Tomorrow on GitHub where you can see entire solution for this problem.
To convert String to Date you use:
let date = Date.fromISO("2020-11-01T21:10:56.22+02:00")
A new option (as of Swift 5.1) is a Property Wrapper. The CodableWrappers library has an easy way to deal with this.
For default ISO8601
#ISO8601DateCoding
struct JustADate: Codable {
var date: Date
}
If you want a custom version:
// Custom coder
#available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
public struct FractionalSecondsISO8601DateStaticCoder: StaticCoder {
private static let iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withFractionalSeconds
return formatter
}()
public static func decode(from decoder: Decoder) throws -> Date {
let stringValue = try String(from: decoder)
guard let date = iso8601Formatter.date(from: stringValue) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Expected date string to be ISO8601-formatted."))
}
return date
}
public static func encode(value: Date, to encoder: Encoder) throws {
try iso8601Formatter.string(from: value).encode(to: encoder)
}
}
// Property Wrapper alias
public typealias ISO8601FractionalDateCoding = CodingUses<FractionalSecondsISO8601DateStaticCoder>
// Usage
#ISO8601FractionalDateCoding
struct JustADate: Codable {
var date: Date
}
Alternatively to #Leo's answer, and if you need to provide support for older OS'es (ISO8601DateFormatter is available only starting with iOS 10, mac OS 10.12), you can write a custom formatter that uses both formats when parsing the string:
class MyISO8601Formatter: DateFormatter {
static let formatters: [DateFormatter] = [
iso8601Formatter(withFractional: true),
iso8601Formatter(withFractional: false)
]
static func iso8601Formatter(withFractional fractional: Bool) -> DateFormatter {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss\(fractional ? ".SSS" : "")XXXXX"
return formatter
}
override public func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?,
for string: String,
errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
guard let date = (type(of: self).formatters.flatMap { $0.date(from: string) }).first else {
error?.pointee = "Invalid ISO8601 date: \(string)" as NSString
return false
}
obj?.pointee = date as NSDate
return true
}
override public func string(for obj: Any?) -> String? {
guard let date = obj as? Date else { return nil }
return type(of: self).formatters.flatMap { $0.string(from: date) }.first
}
}
, which you can use it as date decoding strategy:
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(MyISO8601Formatter())
Although a little bit uglier in implementation, this has the advantage of being consistent with the decoding errors that Swift throws in case of malformed data, as we don't alter the error reporting mechanism).
For example:
struct TestDate: Codable {
let date: Date
}
// I don't advocate the forced unwrap, this is for demo purposes only
let jsonString = "{\"date\":\"2017-06-19T18:43:19Z\"}"
let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(MyISO8601Formatter())
do {
print(try decoder.decode(TestDate.self, from: jsonData))
} catch {
print("Encountered error while decoding: \(error)")
}
will print TestDate(date: 2017-06-19 18:43:19 +0000)
Adding the fractional part
let jsonString = "{\"date\":\"2017-06-19T18:43:19.123Z\"}"
will result in the same output: TestDate(date: 2017-06-19 18:43:19 +0000)
However using an incorrect string:
let jsonString = "{\"date\":\"2017-06-19T18:43:19.123AAA\"}"
will print the default Swift error in case of incorrect data:
Encountered error while decoding: dataCorrupted(Swift.DecodingError.Context(codingPath: [__lldb_expr_84.TestDate.(CodingKeys in _B178608BE4B4E04ECDB8BE2F689B7F4C).date], debugDescription: "Date string does not match format expected by formatter.", underlyingError: nil))

Categories