Skip to content

Commit 3d6ca2b

Browse files
authored
Merge pull request #14 from swhitty/DateEncodingStrategy
Date Encoding/Decoding Strategy
2 parents f2058a2 + c584af0 commit 3d6ca2b

File tree

6 files changed

+236
-64
lines changed

6 files changed

+236
-64
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,34 @@ let user = try KeyValueDecoder().decode(User.self, from: [["id": 1, "name": "Her
5757
let ascii = try KeyValueDecoder().decode([UInt8].self, from: [10, 100, 1000])
5858
```
5959

60+
61+
## Date Encoding/Decoding Strategy
62+
63+
The encoding of `Date` can be adjusted by setting the strategy.
64+
65+
By default `Date` instances are encoded by simply casting to `Any` but this adjusted by setting the strategy.
66+
67+
The default strategy casts to `Any` leaving the instance unchanged:
68+
69+
```swift
70+
var encoder = KeyValueEncoder()
71+
encoder.dateEncodingStrategy = .date
72+
73+
// Date()
74+
let any = try encoder.encode(Date())
75+
```
76+
77+
ISO8601 compatible strings can be used:
78+
79+
```swift
80+
encoder.dateEncodingStrategy = .iso8601()
81+
82+
// "1970-01-01T00:00:00Z"
83+
let any = try encoder.encode(Date(timeIntervalSince1970: 0))
84+
```
85+
86+
Epochs are also supported using `.secondsSince1970` and `millisecondsSince1970`.
87+
6088
## Nil Encoding/Decoding Strategy
6189

6290
The encoding of `Optional.none` can be adjusted by setting the strategy.

Sources/KeyValueDecoder.swift

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,18 @@ public struct KeyValueDecoder: Sendable {
3838
/// Contextual user-provided information for use during encoding.
3939
public var userInfo: [CodingUserInfoKey: any Sendable]
4040

41-
/// The strategy to use for decoding `nil`. Defaults to `Optional<Any>.none` which can be decoded to any optional type.
42-
public var nilDecodingStrategy: NilDecodingStrategy = .default
41+
/// The strategy to use for decoding Date types.
42+
public var dateDecodingStrategy: DateDecodingStrategy = .date
4343

4444
/// The strategy to use for decoding BinaryInteger types. Defaults to `.exact` for lossless conversion between types.
4545
public var intDecodingStrategy: IntDecodingStrategy = .exact
4646

4747
/// The strategy to use for decoding each types keys.
4848
public var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys
4949

50+
/// The strategy to use for decoding `nil`. Defaults to `Optional<Any>.none` which can be decoded to any optional type.
51+
public var nilDecodingStrategy: NilDecodingStrategy = .default
52+
5053
/// Initializes `self` with default strategy.
5154
public init () {
5255
self.userInfo = [:]
@@ -94,6 +97,21 @@ public struct KeyValueDecoder: Sendable {
9497
/// A key encoding strategy that doesn’t change key names during encoding.
9598
case useDefaultKeys
9699
}
100+
101+
public enum DateDecodingStrategy: Sendable {
102+
103+
/// Decodes dates by casting from Any.
104+
case date
105+
106+
/// Decodes dates from ISO8601 strings.
107+
case iso8601(options: ISO8601DateFormatter.Options = [.withInternetDateTime])
108+
109+
/// Decodes dates in terms of milliseconds since midnight UTC on January 1st, 1970.
110+
case millisecondsSince1970
111+
112+
/// Decodes dates in terms of seconds since midnight UTC on January 1st, 1970.
113+
case secondsSince1970
114+
}
97115
}
98116

99117
#if canImport(Combine)
@@ -118,13 +136,15 @@ private extension KeyValueDecoder {
118136
var optionals: NilDecodingStrategy
119137
var integers: IntDecodingStrategy
120138
var keys: KeyDecodingStrategy
139+
var dates: DateDecodingStrategy
121140
}
122141

123142
var strategy: DecodingStrategy {
124143
DecodingStrategy(
125144
optionals: nilDecodingStrategy,
126145
integers: intDecodingStrategy,
127-
keys: keyDecodingStrategy
146+
keys: keyDecodingStrategy,
147+
dates: dateDecodingStrategy
128148
)
129149
}
130150

@@ -336,7 +356,23 @@ private extension KeyValueDecoder {
336356
}
337357

338358
func decode(_ type: Date.Type) throws -> Date {
339-
try getValue()
359+
switch strategy.dates {
360+
case .date:
361+
return try getValue()
362+
case .iso8601(options: let options):
363+
let string = try decode(String.self)
364+
let formatter = ISO8601DateFormatter()
365+
formatter.formatOptions = options
366+
guard let date = formatter.date(from: string) else {
367+
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Failed to decode Date from ISO8601 string \(string)"))
368+
}
369+
return date
370+
case .millisecondsSince1970:
371+
return try Date(timeIntervalSince1970: TimeInterval(decode(Int.self)) / 1000)
372+
373+
case .secondsSince1970:
374+
return try Date(timeIntervalSince1970: TimeInterval(decode(Int.self)))
375+
}
340376
}
341377

342378
func decode(_ type: Data.Type) throws -> Data {

Sources/KeyValueEncoder.swift

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,16 @@ public struct KeyValueEncoder: Sendable {
3737
/// Contextual user-provided information for use during encoding.
3838
public var userInfo: [CodingUserInfoKey: any Sendable]
3939

40-
/// The strategy to use for encoding `nil`. Defaults to `Optional<Any>.none` which can be cast to any optional type.
41-
public var nilEncodingStrategy: NilEncodingStrategy = .default
40+
/// The strategy to use for encoding Date types.
41+
public var dateEncodingStrategy: DateEncodingStrategy = .date
4242

4343
/// The strategy to use for encoding each types keys.
4444
public var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys
4545

46+
/// The strategy to use for encoding `nil`. Defaults to `Optional<Any>.none` which can be cast to any optional type.
47+
public var nilEncodingStrategy: NilEncodingStrategy = .default
48+
49+
4650
/// Initializes `self` with default strategies.
4751
public init () {
4852
self.userInfo = [:]
@@ -67,6 +71,21 @@ public struct KeyValueEncoder: Sendable {
6771
/// A key encoding strategy that doesn’t change key names during encoding.
6872
case useDefaultKeys
6973
}
74+
75+
public enum DateEncodingStrategy: Sendable {
76+
77+
/// Encodes dates by directly casting to Any.
78+
case date
79+
80+
/// Encodes dates from ISO8601 strings.
81+
case iso8601(options: ISO8601DateFormatter.Options = [.withInternetDateTime])
82+
83+
/// Encodes dates to Int in terms of milliseconds since midnight UTC on January 1, 1970.
84+
case millisecondsSince1970
85+
86+
/// Encodes dates to Int in terms of seconds since midnight UTC on January 1, 1970.
87+
case secondsSince1970
88+
}
7089
}
7190

7291
/// Strategy used to encode and decode nil values.
@@ -96,6 +115,12 @@ extension KeyValueEncoder: TopLevelEncoder {
96115

97116
extension KeyValueEncoder {
98117

118+
struct EncodingStrategy {
119+
var optionals: NilEncodingStrategy
120+
var keys: KeyEncodingStrategy
121+
var dates: DateEncodingStrategy
122+
}
123+
99124
static func makePlistCompatible() -> KeyValueEncoder {
100125
var encoder = KeyValueEncoder()
101126
encoder.nilEncodingStrategy = .stringNull
@@ -161,15 +186,11 @@ private extension KeyValueEncoder.NilEncodingStrategy {
161186

162187
private extension KeyValueEncoder {
163188

164-
struct EncodingStrategy {
165-
var optionals: NilEncodingStrategy
166-
var keys: KeyEncodingStrategy
167-
}
168-
169189
var strategy: EncodingStrategy {
170190
EncodingStrategy(
171191
optionals: nilEncodingStrategy,
172-
keys: keyEncodingStrategy
192+
keys: keyEncodingStrategy,
193+
dates: dateEncodingStrategy
173194
)
174195
}
175196

@@ -217,7 +238,7 @@ private extension KeyValueEncoder {
217238
}
218239

219240
func encodeToValue<T>(_ value: T) throws -> EncodedValue where T: Encodable {
220-
guard let encoded = EncodedValue(value) else {
241+
guard let encoded = EncodedValue.makeValue(for: value, using: strategy) else {
221242
try value.encode(to: self)
222243
return try getEncodedValue()
223244
}
@@ -317,7 +338,7 @@ private extension KeyValueEncoder {
317338
}
318339

319340
func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
320-
if let val = EncodedValue(value) {
341+
if let val = EncodedValue.makeValue(for: value, using: strategy) {
321342
setValue(val, forKey: key)
322343
return
323344
}
@@ -447,7 +468,7 @@ private extension KeyValueEncoder {
447468
}
448469

449470
func encode<T: Encodable>(_ value: T) throws {
450-
if let val = EncodedValue(value) {
471+
if let val = EncodedValue.makeValue(for: value, using: strategy) {
451472
appendValue(val)
452473
return
453474
}
@@ -566,7 +587,7 @@ private extension KeyValueEncoder {
566587
}
567588

568589
func encode<T>(_ value: T) throws where T: Encodable {
569-
if let encoded = EncodedValue(value) {
590+
if let encoded = EncodedValue.makeValue(for: value, using: strategy) {
570591
self.value = encoded
571592
return
572593
}
@@ -687,20 +708,32 @@ struct AnyCodingKey: CodingKey {
687708

688709
extension KeyValueEncoder.EncodedValue {
689710

690-
static func isSupportedValue(_ value: Any) -> Bool {
691-
switch value {
692-
case is Data: return true
693-
case is Date: return true
694-
case is URL: return true
695-
case is Decimal: return true
696-
default: return false
711+
static func makeValue(for value: Any, using strategy: KeyValueEncoder.EncodingStrategy) -> Self? {
712+
if let dataValue = value as? Data {
713+
return .value(dataValue)
714+
} else if let dateValue = value as? Date {
715+
return makeValue(for: dateValue, using: strategy.dates)
716+
} else if let urlValue = value as? URL {
717+
return .value(urlValue)
718+
} else if let decimalValue = value as? Decimal {
719+
return .value(decimalValue)
720+
} else {
721+
return nil
697722
}
698723
}
699724

700-
init?(_ value: Any) {
701-
guard Self.isSupportedValue(value) else {
702-
return nil
725+
static func makeValue(for date: Date, using strategy: KeyValueEncoder.DateEncodingStrategy) -> Self? {
726+
switch strategy {
727+
case .date:
728+
return .value(date)
729+
case .iso8601(options: let options):
730+
let f = ISO8601DateFormatter()
731+
f.formatOptions = options
732+
return .value(f.string(from: date))
733+
case .millisecondsSince1970:
734+
return .value(Int(date.timeIntervalSince1970 * 1000))
735+
case .secondsSince1970:
736+
return .value(Int(date.timeIntervalSince1970))
703737
}
704-
self = .value(value)
705738
}
706739
}

Tests/KeyValueDecoderTests.swift

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -403,11 +403,35 @@ struct KeyValueDecoderTests {
403403

404404
@Test
405405
func decodes_Date() throws {
406-
let decoder = KeyValueDecoder()
406+
var decoder = KeyValueDecoder()
407+
let referenceDate = Date(timeIntervalSinceReferenceDate: 0)
408+
409+
decoder.dateDecodingStrategy = .date
410+
#expect(
411+
try decoder.decode(Date.self, from: referenceDate) == referenceDate
412+
)
413+
414+
decoder.dateDecodingStrategy = .iso8601()
415+
#expect(
416+
try decoder.decode(Date.self, from: "2001-01-01T00:00:00Z") == referenceDate
417+
)
418+
#expect(throws: DecodingError.self) {
419+
try decoder.decode(Date.self, from: "2001-01-01")
420+
}
421+
422+
decoder.dateDecodingStrategy = .iso8601(options: [.withInternetDateTime, .withFractionalSeconds])
423+
#expect(
424+
try decoder.decode(Date.self, from: "2001-01-01T00:00:00.000Z") == referenceDate
425+
)
426+
427+
decoder.dateDecodingStrategy = .millisecondsSince1970
428+
#expect(
429+
try decoder.decode(Date.self, from: 978307200000) == referenceDate
430+
)
407431

408-
let date = Date(timeIntervalSinceReferenceDate: 0)
432+
decoder.dateDecodingStrategy = .secondsSince1970
409433
#expect(
410-
try decoder.decode(Date.self, from: date) == date
434+
try decoder.decode(Date.self, from: 978307200) == referenceDate
411435
)
412436
}
413437

0 commit comments

Comments
 (0)