I would like to create an extension to Date
, or NSDate
, with a custom initializer that parses date strings in the format yyyy-MM-dd
. I need this to have a different symbol name, so I can't create an actual extension
to the best of my knowledge.
I have written the following, but the self.init
line is evidently invalid, and I haven't been able to figure out how to finalize initialization:
class DateOnly: NSDate, @unchecked Sendable {
convenience init(from: String) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX") as Locale
let d = formatter.date(from: from)!
self.init(timeInterval: 0, since: d)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
So my question is: how can I update this code to work as intended? Or what code do I need to write to accomplish my goal?
For a little more background:
I already have a couple of other extensions to support RFC 3339 date-time
strings:
extension Formatter {
static func rfc3339Formatter() -> ISO8601DateFormatter {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}
}
extension JSONDecoder.DateDecodingStrategy {
static let rfc3339 = custom { decoder in
let dateStr = try decoder.singleValueContainer().decode(String.self)
let formatter = Formatter.rfc3339Formatter()
if let date = formatter.date(from: dateStr) {
return date
}
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Invalid date"
)
)
}
}
This works as expected for payloads that I need to parse which only contain full-date
strings. For example, I am able to add a method to my structs that provide a preconfigured decoder:
static let decoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .rfc3339
return decoder
}()
But I need to parse payloads that will have both types of strings to parse. So I want to be able to write a struct like:
struct Foo: Decodable {
let date1: Date // This is an RFC 3339 `date-time` string
let date2: MyCustomDate // This is a simple `yyyy-MM-dd` string
static let decoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .rfc3339
return decoder
}()
}
And then implement whatever decoding protocol or such that I need to for MyCustomDate
that would transparently allow for parsing a JSON representation of Foo
.
What you need is to adjust your custom JSONDecoder.DateDecodingStrategy
:
iOS 15 or later
extension ParseStrategy where Self == Date.ISO8601FormatStyle {
static var iso8601withFractionalSeconds: Self { .init(includingFractionalSeconds: true) }
static var iso8601withoutTime: Self { iso8601.year().month().day() }
}
extension JSONDecoder.DateDecodingStrategy {
static let iso8601withOptionalTime = custom {
let string = try $0.singleValueContainer().decode(String.self)
do {
return try .init(string, strategy: .iso8601withFractionalSeconds)
} catch {
return try .init(string, strategy: .iso8601withoutTime)
}
}
}
Playground testing
struct ISOWithOpionalTimeDates: Codable {
let dateWithFractionalSeconds: Date
let dateWithoutTime: Date
}
let isoDatesJSON = """
{
"dateWithFractionalSeconds": "2017-06-19T18:43:19.123Z",
"dateWithoutTime": "2017-06-19",
}
"""
let isoDatesData = Data(isoDatesJSON.utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601withOptionalTime
do {
let isoDates = try decoder.decode(ISOWithOpionalTimeDates.self, from: isoDatesData)
print(isoDates)
} catch {
print(error)
}
This will print
ISOWithOpionalTimeDates(dateWithFractionalSeconds: 2017-06-19 18:43:19 +0000, dateWithoutTime: 2017-06-19 00:00:00 +0000)
If you need to support older than iOS15 you can check this post