Almost iSO8601 format - ios

I need to parse the following date in a JSON payload and it's almost in ISO8601 format.
"2020-06-05 14:52:54 UTC"
To conform to ISO8601 it needs to be altered slightly.
"2020-06-05T14:52:54Z"
It's super annoying because I now have to make a customer date decoding strategy.
static func make() -> JSONDecoder {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
// decoder.dateDecodingStrategy = .iso8601
decoder.dateDecodingStrategy = .custom({ decoder in
let container = try decoder.singleValueContainer()
let dateStr = try container.decode(String.self)
guard let date = formatter.date(from: dateStr) else {
preconditionFailure("Unexpected date format.")
}
return date
})
return decoder
}
I don't have control of the data source. Is there anything I can do to avoid a custom decoding strategy in this case?

Since you seem to just want to use a DateFormatter to parse the date string, use the formatted strategy.
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss zzzz"
// or
// formatter.dateFormat = "yyyy-MM-dd HH:mm:ss 'UTC'"
// formatter.timeZone = .init(identifier: "UTC")
formatter.locale = Locale(identifier: "en_US_POSIX")
decoder.dateDecodingStrategy = .formatted(formatter)

An alternative to Sweeper's answer is to write an extension of DateFormatter
extension DateFormatter {
static let almostISO8601Formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
return formatter
}()
}
Then your make() function simply becomes
static func make() -> JSONDecoder {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(.almostISO8601Formatter)
return decoder
}
Consider also to put make() (with a more meaningful name) in an extension of JSONDecoder

You could map your input into iso8601, to use an existing decoder:
func iso8601ify(_ str: String) -> String {
str.split(separator: " ")
.prefix(2)
.joined(separator: "T")
.appending("Z")
}
This example, there's no error handling, of course

Related

Using a custom ISO8601DateFormatter with JSONEncoder [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))

String can't be parsed into Date "Swift"

I'm trying to parse the following String input "2020-04-05 19:02:02" into Date, which I'm using the following code to do that:
static func getDateFromString(_ dateStr: String) -> Date? {
let dateFormater = DateFormatter()
dateFormater.dateFormat = "yyyy-MM-dd H:mm:ss"
dateFormater.timeZone = TimeZone(identifier: "UTC")
return dateFormater.date(from: dateStr)
}
The problem is that not working, it returns nil
Any suggestion? Thank you
Here you go, this works fine:
In dateString pass data in string format and for dateFormat pass format you want and to choose format use NSDateFormatter:
let expiryDateString = "2020-04-05 19:02:02"
let dateformat = "yyyy-MM-dd HH:mm:ss"
let expireDate = DateHelper.getDateFrom(expireDateString, dateformat)
static func getDateFrom(dateString: String,dateFormat: String) -> Date? {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = dateFormat
dateFormatter.timeZone = .current
guard let date = dateFormatter.date(from: dateString) else {return nil}
return date
}

How can I use JSONDecoder / Codable with this date format?

I get the following date back from an API.
{
"createdDate": "2019-03-22T15:53:06.663Z"
}
I'd like to decode this and store it as a Date type.
My JSONDecoder is not able to decode this however.
I have tried to extend it with
extension DateFormatter {
static let iso8601Full: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
formatter.calendar = Calendar(identifier: .iso8601)
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()
}
and then using decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full) but this does not work
Use ISO8601DateFormatter with formatting options
let str = "2019-03-22T15:53:06.663Z"
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate, .withFullTime, .withTimeZone, .withFractionalSeconds]
let date = formatter.date(from: str)

Convert NSDate to String in iOS Swift [duplicate]

This question already has answers here:
Convert NSDate to NSString
(19 answers)
Closed 5 years ago.
I am trying to convert a NSDate to a String and then Change Format. But when I pass NSDate to String it is producing whitespace.
let formatter = DateFormatter()
let myString = (String(describing: date))
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let yourDate: Date? = formatter.date(from: myString)
formatter.dateFormat = "dd-MMM-yyyy"
print(yourDate)
you get the detail information from Apple Dateformatter Document.If you want to set the dateformat for your dateString, see this link , the detail dateformat you can get here
for e.g , do like
let formatter = DateFormatter()
// initially set the format based on your datepicker date / server String
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let myString = formatter.string(from: Date()) // string purpose I add here
// convert your string to date
let yourDate = formatter.date(from: myString)
//then again set the date format whhich type of output you need
formatter.dateFormat = "dd-MMM-yyyy"
// again convert your date to string
let myStringDate = formatter.string(from: yourDate!)
print(myStringDate)
you get the output as
I always use this code while converting Date to String . (Swift 3)
extension Date
{
func toString( dateFormat format : String ) -> String
{
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format
return dateFormatter.string(from: self)
}
}
and call like this . .
let today = Date()
today.toString(dateFormat: "dd-MM")
DateFormatter has some factory date styles for those too lazy to tinker with formatting strings. If you don't need a custom style, here's another option:
extension Date {
func asString(style: DateFormatter.Style) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = style
return dateFormatter.string(from: self)
}
}
This gives you the following styles:
short, medium, long, full
Example usage:
let myDate = Date()
myDate.asString(style: .full) // Wednesday, January 10, 2018
myDate.asString(style: .long) // January 10, 2018
myDate.asString(style: .medium) // Jan 10, 2018
myDate.asString(style: .short) // 1/10/18
Your updated code.update it.
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let myString = formatter.string(from: date as Date)
let yourDate: Date? = formatter.date(from: myString)
formatter.dateFormat = "dd-MMM-yyyy"
print(yourDate!)
Something to keep in mind when creating formatters is to try to reuse the same instance if you can, as formatters are fairly computationally expensive to create. The following is a pattern I frequently use for apps where I can share the same formatter app-wide, adapted from NSHipster.
extension DateFormatter {
static var sharedDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
// Add your formatter configuration here
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter
}()
}
Usage:
let dateString = DateFormatter.sharedDateFormatter.string(from: Date())
After allocating DateFormatter you need to give the formatted string
then you can convert as string like this way
var date = Date()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let myString = formatter.string(from: date)
let yourDate: Date? = formatter.date(from: myString)
formatter.dateFormat = "dd-MMM-yyyy"
let updatedString = formatter.string(from: yourDate!)
print(updatedString)
OutPut
01-Mar-2017
You can use this extension:
extension Date {
func toString(withFormat format: String) -> String {
let formatter = DateFormatter()
formatter.dateFormat = format
let myString = formatter.string(from: self)
let yourDate = formatter.date(from: myString)
formatter.dateFormat = format
return formatter.string(from: yourDate!)
}
}
And use it in your view controller like this (replace <"yyyy"> with your format):
yourString = yourDate.toString(withFormat: "yyyy")

ISO8601DateFormatter doesn't parse ISO date string

I'm trying to parse this
2017-01-23T10:12:31.484Z
using native ISO8601DateFormatter class provided by iOS 10 but always fails.
If the string not contains milliseconds, the Date object is created without problems.
I'm tried this and many options combination but always fails...
let formatter = ISO8601DateFormatter()
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.formatOptions = [.withInternetDateTime, .withDashSeparatorInDate, .withColonSeparatorInTime, .withColonSeparatorInTimeZone, .withFullTime]
Any idea?
Thanks!
Prior to macOS 10.13 / iOS 11 ISO8601DateFormatter does not support date strings including milliseconds.
A workaround is to remove the millisecond part with regular expression.
let isoDateString = "2017-01-23T10:12:31.484Z"
let trimmedIsoString = isoDateString.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
let formatter = ISO8601DateFormatter()
let date = formatter.date(from: trimmedIsoString)
In macOS 10.13+ / iOS 11+ a new option is added to support fractional seconds:
static var withFractionalSeconds: ISO8601DateFormatter.Options { get }
let isoDateString = "2017-01-23T10:12:31.484Z"
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let date = formatter.date(from: isoDateString)
For people that are not ready to go to iOS 11 yet, you can always create your own formatter to handle milliseconds, e.g.:
extension DateFormatter {
static var iSO8601DateWithMillisec: DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
return dateFormatter
}
}
Usage:
let formater = DateFormatter.iSO8601DateWithMillisec
let date = formater.date(from: "2017-01-23T10:12:31.484Z")!
print(date) // output: 2017-01-23 10:12:31 +0000
It is slightly more elegant than writing a regex to strip out the milliseconds from the input string.
Maybe this will help to decode slightly different formats:
extension JSONDecoder {
enum DateDecodeError: String, Error {
case invalidDate
}
static var bestDateAttemptDecoder: JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
if let dateSecs = try? container.decode(Double.self) {
return Date(timeIntervalSince1970: dateSecs)
}
if let dateSecs = try? container.decode(UInt.self) {
return Date(timeIntervalSince1970: TimeInterval(dateSecs))
}
let dateStr = try container.decode(String.self)
let isoFormatter = ISO8601DateFormatter()
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = isoFormatter.date(from: dateStr) {
return date
}
isoFormatter.formatOptions = [.withInternetDateTime ]
if let date = isoFormatter.date(from: dateStr) {
return date
}
log.warning("Cannot decode date");
throw DateDecodeError.invalidDate
})
return decoder
}
}
From: https://gist.github.com/th3m477/442a0d1da6354dd3b84e3b71df5dca6a
I encountered same issue some months ago. And here's my solution for reference:
// *****************************************
// MARK: - Formatter extension
// *****************************************
extension Formatter {
static let iso8601: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.timeZone = TimeZone.current
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}()
static let iso8601NoSecond: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.timeZone = TimeZone.current
formatter.formatOptions = [.withInternetDateTime]
return formatter
}()
}
// *****************************************
// MARK: - ISO8601 helper
// *****************************************
func getDateFrom(DateString8601 dateString:String) -> Date?
{
if let date = Formatter.iso8601.date(from: dateString) {
return date
}
if let date = Formatter.iso8601NoSecond.date(from: dateString) {
return date
}
return nil
}
// *****************************************
// usage
// *****************************************
let d = getDateFrom(DateString8601: "2017-01-23T10:12:31.484Z")
print("2017-01-23T10:12:31.484Z millis= ", d?.timeIntervalSinceReferenceDate)
let d2 = getDateFrom(DateString8601: "2017-01-23T10:12:31Z")
print("2017-01-23T10:12:31Z millis= ", d2?.timeIntervalSinceReferenceDate)
// *****************************************
// result
// *****************************************
2017-01-23T10:12:31.484Z millis= Optional(506859151.48399997)
2017-01-23T10:12:31Z millis= Optional(506859151.0)

Resources