Skip to content

Commit

Permalink
#8 Add Support for ISO8601 Formatter With Millisecond Precision
Browse files Browse the repository at this point in the history
  • Loading branch information
JARMourato committed Sep 1, 2021
2 parents 4ecaf11 + 7fd53c5 commit f43b0ec
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 41 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ let user = try User.decode(from: json)
#### CodableDate

This wrapper allows decoding dates on per-property strategy basis. By default, `CodableDate` uses the `iso8601` strategy. The built-in strategies are:
`iso8601, rfc2822, rfc3339 and timestamp`. There is also the option of using a custom format by providing a valid string format to the option `.format()`.
`iso8601`, `iso8601WithMillisecondPrecision`, `rfc2822`, `rfc3339`, and `timestamp`. There is also the option of using a custom format by providing a valid string format to the option `.format()`.

```Swift
struct Dates: Kodable {
Expand Down Expand Up @@ -170,6 +170,7 @@ print(dates.rfc3339Date.description) // Prints "1996-12-20 00:39:57 +0000"
print(dates.timestamp.description) // Prints "2001-01-01 00:00:00 +0000"
````

Note that there's no built-in support for ISO8601 dates with precision greater than millisecond (e.g. microsecond or nanosecond), because Apple doesn't officially supports such precision natively, yet. Should you feel the necessity to have those, PRs are always welcome!

## Advanced Usage

Expand Down
18 changes: 18 additions & 0 deletions Sources/Wrappers/CodableDate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,27 @@ extension Optional: DateProtocol where Wrapped == Date {}
// MARK: Coding Strategy

public enum DateCodingStrategy {
/// Custom date formatter.
case format(String)
/// Uses the iOS native `ISO8601DateFormatter`.
case iso8601
/// Uses the iOS native `ISO8601DateFormatter` with `DateFormatter.Options.withFractionalSeconds`.
/// - Note: If you pass a date with greater precision than milliseconds, it will truncate and parse up to milliseconds only. So "2021-08-30T18:35:19.209999Z" would end up being parsed as "2021-08-30T18:35:19.209Z".
case iso8601WithMillisecondPrecision
/// Implements the RFC2822 date format: "EEE, d MMM y HH:mm:ss zzz"
/// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc2822
case rfc2822
/// Implements the RFC3339 date format: "yyyy-MM-dd'T'HH:mm:ssZ"
/// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc3339
case rfc3339
/// Time interval since 1970.
case timestamp

public func date(from value: String) -> Date? {
switch self {
case let .format(format): return DateCodingStrategy.getFormatter(format).date(from: value)
case .iso8601: return DateCodingStrategy.iso8601Formatter.date(from: value)
case .iso8601WithMillisecondPrecision: return DateCodingStrategy.iso8601WithFractionalSecondsFormatter.date(from: value)
case .rfc2822: return DateCodingStrategy.rfc2822Formatter.date(from: value)
case .rfc3339: return DateCodingStrategy.rfc3339Formatter.date(from: value)
case .timestamp:
Expand All @@ -91,13 +102,20 @@ public enum DateCodingStrategy {
switch self {
case let .format(format): return DateCodingStrategy.getFormatter(format).string(from: date)
case .iso8601: return DateCodingStrategy.iso8601Formatter.string(from: date)
case .iso8601WithMillisecondPrecision: return DateCodingStrategy.iso8601WithFractionalSecondsFormatter.string(from: date)
case .rfc2822: return DateCodingStrategy.rfc2822Formatter.string(from: date)
case .rfc3339: return DateCodingStrategy.rfc3339Formatter.string(from: date)
case .timestamp: return "\(date.timeIntervalSince1970)"
}
}

private static let iso8601Formatter = ISO8601DateFormatter()
private static let iso8601WithFractionalSecondsFormatter: ISO8601DateFormatter = {
let result = ISO8601DateFormatter()
result.formatOptions.insert(.withFractionalSeconds)
return result
}()

private static let rfc2822Formatter: DateFormatter = getFormatter("EEE, d MMM y HH:mm:ss zzz")
private static let rfc3339Formatter: DateFormatter = getFormatter("yyyy-MM-dd'T'HH:mm:ssZ")
private static var formatters: [String: Formatter] = [:]
Expand Down
95 changes: 55 additions & 40 deletions Tests/KodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -601,10 +601,11 @@ final class KodableTests: XCTestCase {
XCTAssertEqual(try optionalTransformer.transformFromJSON(value: nil), nil)
}

func testCodableDate() {
func testCodableDate() throws {
struct Dates: Kodable {
@CodableDate(decoding: .enforceType) var iso8601: Date
@CodableDate("iso8601") var isoDate: Date?
@CodableDate(.iso8601WithMillisecondPrecision, "iso8601_millisecond_date") var isoNanosecondDate: Date?
@CodableDate(.format("y-MM-dd"), "simple_date") var simpleDate: Date
@CodableDate(.rfc2822, "rfc2822") var rfc2822Date: Date
@CodableDate(.rfc3339, "rfc3339") var rfc3339Date: Date
Expand All @@ -624,46 +625,59 @@ final class KodableTests: XCTestCase {
@CodableDate(.rfc2822, "rfc3339") var duplicateIso: Date
}

do {
// Regular codable
let codableDecoded = try CodableDates.decodeJSON(from: KodableTests.json)
XCTAssertEqual(codableDecoded.iso8601.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(codableDecoded.duplicateIso.description, "1996-12-20 00:39:57 +0000")

let encodableJson = try codableDecoded.encodeJSON()
let newCodableObject = try CodableDates.decodeJSON(from: encodableJson)
XCTAssertEqual(newCodableObject.iso8601.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(newCodableObject.duplicateIso.description, "1996-12-20 00:39:57 +0000")

// ExtendedCodable
let decoded = try Dates.decodeJSON(from: KodableTests.json)
XCTAssertEqual(decoded.iso8601.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(decoded.isoDate?.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(decoded.simpleDate.description, "2001-01-01 00:00:00 +0000")
XCTAssertEqual(decoded.rfc2822Date.description, "1996-12-19 16:39:57 +0000")
XCTAssertEqual(decoded.rfc3339Date.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(decoded.nonOptionalTimestamp.description, "2001-01-01 00:00:00 +0000")
XCTAssertEqual(decoded.timestamp?.description, "2001-01-01 00:00:00 +0000")
XCTAssertEqual(decoded.defaultTimestamp.description, KodableTests.testDate.description)
XCTAssertEqual(decoded.optionalDate, nil)
XCTAssertEqual(decoded.ignoredDate, nil)

let json = try decoded.encodeJSON()
let newObject = try Dates.decodeJSON(from: json)

XCTAssertEqual(decoded.iso8601.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(newObject.isoDate?.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(newObject.simpleDate.description, "2001-01-01 00:00:00 +0000")
XCTAssertEqual(newObject.rfc2822Date.description, "1996-12-19 16:39:57 +0000")
XCTAssertEqual(newObject.rfc3339Date.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(newObject.nonOptionalTimestamp.description, "2001-01-01 00:00:00 +0000")
XCTAssertEqual(newObject.timestamp?.description, "2001-01-01 00:00:00 +0000")
XCTAssertEqual(newObject.defaultTimestamp.description, KodableTests.testDate.description)
XCTAssertEqual(newObject.optionalDate, nil)
XCTAssertEqual(newObject.ignoredDate, nil)
} catch {
XCTFail(error.localizedDescription)
// Regular codable
let codableDecoded = try CodableDates.decodeJSON(from: KodableTests.json)
XCTAssertEqual(codableDecoded.iso8601.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(codableDecoded.duplicateIso.description, "1996-12-20 00:39:57 +0000")

let encodableJson = try codableDecoded.encodeJSON()
let newCodableObject = try CodableDates.decodeJSON(from: encodableJson)
XCTAssertEqual(newCodableObject.iso8601.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(newCodableObject.duplicateIso.description, "1996-12-20 00:39:57 +0000")

// ExtendedCodable
let decoded = try Dates.decodeJSON(from: KodableTests.json)
XCTAssertEqual(decoded.iso8601.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(decoded.isoDate?.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(decoded.isoNanosecondDate?.description, "2021-08-30 18:35:19 +0000")
XCTAssertEqual(decoded.simpleDate.description, "2001-01-01 00:00:00 +0000")
XCTAssertEqual(decoded.rfc2822Date.description, "1996-12-19 16:39:57 +0000")
XCTAssertEqual(decoded.rfc3339Date.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(decoded.nonOptionalTimestamp.description, "2001-01-01 00:00:00 +0000")
XCTAssertEqual(decoded.timestamp?.description, "2001-01-01 00:00:00 +0000")
XCTAssertEqual(decoded.defaultTimestamp.description, KodableTests.testDate.description)
XCTAssertEqual(decoded.optionalDate, nil)
XCTAssertEqual(decoded.ignoredDate, nil)

let json = try decoded.encodeJSON()
let newObject = try Dates.decodeJSON(from: json)

XCTAssertEqual(newObject.isoDate?.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(newObject.isoNanosecondDate?.description, "2021-08-30 18:35:19 +0000")
XCTAssertEqual(newObject.simpleDate.description, "2001-01-01 00:00:00 +0000")
XCTAssertEqual(newObject.rfc2822Date.description, "1996-12-19 16:39:57 +0000")
XCTAssertEqual(newObject.rfc3339Date.description, "1996-12-20 00:39:57 +0000")
XCTAssertEqual(newObject.nonOptionalTimestamp.description, "2001-01-01 00:00:00 +0000")
XCTAssertEqual(newObject.timestamp?.description, "2001-01-01 00:00:00 +0000")
XCTAssertEqual(newObject.defaultTimestamp.description, KodableTests.testDate.description)
XCTAssertEqual(newObject.optionalDate, nil)
XCTAssertEqual(newObject.ignoredDate, nil)
}

func testISO8601DateCodingStrategy() {
// Our tests don't cover microsecond and nanosecond precision dates because our date formatters don't actually support them yet.
let iso8601 = "2021-08-30T18:35:19Z"
let iso8601WithMillisecondPrecision = "2021-08-30T18:35:19.999Z"

func assert(_ dateString: String, against formatter: DateCodingStrategy) {
if let date = formatter.date(from: dateString) {
XCTAssertEqual(formatter.string(from: date), dateString)
} else {
XCTFail("Failed to initialize date from \(dateString) using \(formatter)")
}
}
assert(iso8601, against: .iso8601)
assert(iso8601WithMillisecondPrecision, against: .iso8601WithMillisecondPrecision)
}

func testFailedCodableDate() {
Expand Down Expand Up @@ -764,6 +778,7 @@ final class KodableTests: XCTestCase {
"parts": ["first": "random address"],
],
"iso8601": "1996-12-19T16:39:57-08:00",
"iso8601_millisecond_date": "2021-08-30T18:35:19.001Z",
"duplicateIso": "1996-12-19T16:39:57-08:00",
"simple_date": "2001-01-01",
"rfc2822": "Thu, 19 Dec 1996 16:39:57 GMT",
Expand Down

0 comments on commit f43b0ec

Please sign in to comment.