How to Parse Iso 8601 Using Nsdateformatter with Optional Milliseconds Part

How to parse ISO 8601 using NSDateFormatter with optional milliseconds part

As far as I know there is no way to make optional parameters.

The usual solution is to use two formatters, one for each format.
To decide which formatter to use, you can either

  1. Count the number of characters in the date string (as suggested in Parsing a RFC 822 date with NSDateFormatter)

  2. Just try both formatters and get the first non-nil result.

Since your date formats are similar, you can go with only one formatter and if the date string is too short, append .000 before using the formatter.

Swift DateFormatter Optional Milliseconds

Two suggestions:

  • Convert the string with the date format including the milliseconds. If it returns nil convert it with the other format.

  • Strip the milliseconds from the string with Regular Expression:

    var dateString = "2018-01-21T20:11:20.057Z"
    dateString = dateString.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
    // -> 2018-01-21T20:11:20Z

Edit:

To use it with Codable you have to write a custom initializer, specifying dateDecodingStrategy does not work

struct Foo: Decodable {
let birthDate : Date
let name : String

private enum CodingKeys : String, CodingKey { case born, name }

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var rawDate = try container.decode(String.self, forKey: .born)
rawDate = rawDate.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
birthDate = ISO8601DateFormatter().date(from: rawDate)!
name = try container.decode(String.self, forKey: .name)
}
}

let jsonString = """
[{"name": "Bob", "born": "2018-01-21T20:11:20.057Z"}, {"name": "Matt", "born": "2018-01-21T20:11:20Z"}]
"""

do {
let data = Data(jsonString.utf8)
let result = try JSONDecoder().decode([Foo].self, from: data)
print(result)
} catch {
print("error: ", error)
}

Parsing ISO 8601 with NSDateFormatter

Three things:

  • Use the -[NSDateFormatter getObjectValue:forString:range:error:] method to learn how much of the string was parsed and what error prevented parsing further.

  • The ZZZZ format string corresponds to something like "GMT+02:00", with that "GMT" in there. You may need to inject that into the string you're parsing for it to work.

  • Apple's Data Formatting Guide advises "Consider Unix Functions for Fixed-Format, Unlocalized Dates".

How can I parse / create a date time stamp formatted with fractional seconds UTC timezone (ISO 8601, RFC 3339) in Swift?

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"]

enter image description here

ISO8601DateFormatter doesn't parse ISO date string

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)

how do I parse an iso 8601 date (with optional milliseconds) to a struct tm in C++?

You can use C's sscanf (http://www.cplusplus.com/reference/cstdio/sscanf/) to parse it:

const char *dateStr = "2014-11-12T19:12:14.505Z";
int y,M,d,h,m;
float s;
sscanf(dateStr, "%d-%d-%dT%d:%d:%fZ", &y, &M, &d, &h, &m, &s);

If you have std::string it can be called like this (http://www.cplusplus.com/reference/string/string/c_str/):

std::string dateStr = "2014-11-12T19:12:14.505Z";
sscanf(dateStr.c_str(), "%d-%d-%dT%d:%d:%fZ", &y, &M, &d, &h, &m, &s);

If it should handle different timezones you need to use sscanf return value - number of parsed arguments:

int tzh = 0, tzm = 0;
if (6 < sscanf(dateStr.c_str(), "%d-%d-%dT%d:%d:%f%d:%dZ", &y, &M, &d, &h, &m, &s, &tzh, &tzm)) {
if (tzh < 0) {
tzm = -tzm; // Fix the sign on minutes.
}
}

And then you can fill tm (http://www.cplusplus.com/reference/ctime/tm/) struct:

tm time = { 0 };
time.tm_year = y - 1900; // Year since 1900
time.tm_mon = M - 1; // 0-11
time.tm_mday = d; // 1-31
time.tm_hour = h; // 0-23
time.tm_min = m; // 0-59
time.tm_sec = (int)s; // 0-61 (0-60 in C++11)

It also can be done with std::get_time (http://en.cppreference.com/w/cpp/io/manip/get_time) since C++11 as @Barry mentioned in comment how do I parse an iso 8601 date (with optional milliseconds) to a struct tm in C++?

Date Format Swift - More than one input format

You'll have to try both formats, one after the other:

func ISOStringToLocal(_ dateString: String) -> String {
let dateFormatterGet = DateFormatter()
dateFormatterGet.timeZone = TimeZone(identifier: "UTC")
dateFormatterGet.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" // with seconds

let dateFormatterPrint = DateFormatter()
dateFormatterPrint.dateFormat = "dd-MMM-yyyy hh:mm:ss"

if let date = dateFormatterGet.date(from: dateString) {
return dateFormatterPrint.string(from: date)
} else {
dateFormatterGet.dateFormat = "yyyy-MM-dd'T'HH:mm:ss:SSSZZZZZ" // with millis
if let date = dateFormatterGet.date(from: dateString) {
return dateFormatterPrint.string(from: date)
}
return ""
}
}

print(ISOStringToLocal("2017-07-01T13:24:00+02:00"))
print(ISOStringToLocal("2017-07-01T13:24:00:123+02:00"))

How do I get an ISO 8601 date on iOS?

Use NSDateFormatter:

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];   
NSLocale *enUSPOSIXLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
[dateFormatter setLocale:enUSPOSIXLocale];
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"];
[dateFormatter setCalendar:[NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]];

NSDate *now = [NSDate date];
NSString *iso8601String = [dateFormatter stringFromDate:now];

And in Swift:

let dateFormatter = DateFormatter()
let enUSPosixLocale = Locale(identifier: "en_US_POSIX")
dateFormatter.locale = enUSPosixLocale
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
dateFormatter.calendar = Calendar(identifier: .gregorian)

let iso8601String = dateFormatter.string(from: Date())

Swift ISO8601 format to Date

You can specify ISO8601 date formate to the NSDateFormatter to get Date:

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmssZ"
print(dateFormatter.date(from: dateString)) //2018-02-07 12:46:00 +0000


Related Topics



Leave a reply



Submit