BetterCodable icon indicating copy to clipboard operation
BetterCodable copied to clipboard

How to deal with optional Date?

Open tkirby opened this issue 2 years ago • 2 comments

Tried:

@DateValue<ISO8601Strategy> var cancellableUntil: Date?

This gives the error:

Property type 'Date?' does not match 'wrappedValue' type 'Date'

tkirby avatar Nov 02 '22 09:11 tkirby

You can add it to your own project where you use BetterCodable.

import Foundation
import BetterCodable

@propertyWrapper
public struct OptionalDateValue<Formatter: DateValueCodableStrategy> {
    private let value: Formatter.RawValue?
    public var wrappedValue: Date?
    
    public init(wrappedValue: Date?) {
        self.wrappedValue = wrappedValue
        
        if let value = wrappedValue {
            self.value = Formatter.encode(value)
        } else {
            self.value = nil
        }
    }
}

extension OptionalDateValue: Decodable where Formatter.RawValue: Decodable {
    public init(from decoder: Decoder) throws {
        self.value = try Formatter.RawValue?(from: decoder)
        
        if let value = value {
            self.wrappedValue = try Formatter.decode(value)
        } else {
            self.wrappedValue = nil
        }
    }
}

extension OptionalDateValue: Encodable where Formatter.RawValue: Encodable {
    public func encode(to encoder: Encoder) throws {
        try value.encode(to: encoder)
    }
}

extension KeyedDecodingContainer {
    func decode<T>(_ type: OptionalDateValue<T>.Type, forKey key: Self.Key) throws -> OptionalDateValue<T>
    where T.RawValue: Decodable
    {
        try decodeIfPresent(type, forKey: key) ?? OptionalDateValue<T>(wrappedValue: nil)
    }

    func decodeIfPresent<T>(_ type: OptionalDateValue<T>.Type, forKey key: Self.Key) throws -> OptionalDateValue<T>
    where T.RawValue == String
    {
        let stringOptionalValue = try decodeIfPresent(String.self, forKey: key)

        guard let stringValue = stringOptionalValue else {
            return .init(wrappedValue: nil)
        }

        let dateValue = try T.decode(stringValue)
        return .init(wrappedValue: dateValue)
    }
}

And here are some tests:

import XCTest
@testable import YOUR_PROJECT
import BetterCodable

class OptionalDateValueTests: XCTestCase {
    struct TestData: Codable {
        @OptionalDateValue<ISO8601WithFractionalSecondsStrategy>
        var date: Date?
    }
    
    func testDecodingTimestamp() {
        let jsonData = #"{"date": "1984-11-09T00:00:00.000Z"}"#.data(using: .utf8)!
        
        do {
            let data = try JSONDecoder().decode(TestData.self, from: jsonData)
            print(data)
        } catch {
            XCTFail("shouldn't throw: \(error)")
        }
    }
    
    func testDecodingNil() {
        let jsonData = #"{"date": null}"#.data(using: .utf8)!
        
        do {
            let data = try JSONDecoder().decode(TestData.self, from: jsonData)
            XCTAssertNil(data.date)
        } catch {
            XCTFail("shouldn't throw: \(error)")
        }
    }
    
    func testDecodingNotPresent() {
        let jsonData = #"{}"#.data(using: .utf8)!
        
        do {
            let data = try JSONDecoder().decode(TestData.self, from: jsonData)
            XCTAssertNil(data.date)
        } catch {
            XCTFail("shouldn't throw: \(error)")
        }
    }
}

dimat avatar Jul 02 '23 23:07 dimat

I believe there is a bug. The Encodable extension on OptionalDateValue should try to encode the wrappedValue instead of value

extension OptionalDateValue: Encodable where Formatter.RawValue: Encodable {
    public func encode(to encoder: Encoder) throws {
        try wrappedValue.encode(to: encoder)
    }
}

moshegutman avatar Oct 12 '23 19:10 moshegutman