Date.ParseStrategy(dateString:) overload?

So I like a lot about how the new Date formatting options work, but sometimes I find the "format" part super verbose as I already have a whole cheat sheet of the styles I use and a String is a lot easier to store and pass around than a Date.FormatString..

So I'd love an overload like Date.ParseStrategy(formatLiteral:"YYYY-MM-dd' 'HH:mm:ss" , etc.)

Looking at:

I think I see a couple of ways to make it happen, but raw value is internal so I don't think I can do it as just an extension?

extension Date {
    public struct FormatString : Hashable, Sendable {
        internal var rawFormat: String = ""
    }
}

am I missing something simple to make it work already? I tried both

  • Date.FormatString("YYYY-MM-dd' 'HH:mm:ss")
  • Date.FormatString("YYYY-MM-dd HH:mm:ss")

Which compile but fail the test (expected when you look at the inits for FormatString, specifically what asDateFormatLiteral() does.)

The test.

func testDateFormats() throws {
        let dateString = "2024-03-12 03:12:10"
        
        //in real code this lives in an extension, hence vars for now
        
        //.description
        // \'YYYY-MM-dd\'\' \'\'HH:mm:ss\'
        var withTime_fails1:Date.FormatString {
            "YYYY-MM-dd' 'HH:mm:ss"
        }
        
        //.description
        // \'YYYY-MM-dd HH:mm:ss\'
        var withTime_fails2:Date.FormatString {
            "YYYY-MM-dd HH:mm:ss"
        }
        
        //.description
        // y\'-\'MM\'-\'dd\' \'HH\':\'mm\':\'ss
        var withTime_passes:Date.FormatString {
            "\(year: .defaultDigits)-\(month: .twoDigits)-\(day: .twoDigits) \(hour: .twoDigits(clock: .twentyFourHour, hourCycle: .zeroBased)):\(minute: .twoDigits):\(second: .twoDigits)"
        }
        
        //cant use the local var in real code as default value so this init is LONG. 
        func _decodeDateNew(_ value:String, format:Date.FormatString = withTime_passes) throws -> Date {
            let strategy = Date.ParseStrategy(format:  format, timeZone: .gmt)
            
            //format for error message not that useful.
            guard let date = try? Date(value, strategy: strategy) else {
                throw DecodingError.dataCorrupted(
                    .init(codingPath: [], debugDescription: "String not in expected \(strategy.format.description) format.")
                )
            }
            return date
        }
        let dateValue = try _decodeDateNew(dateString, format: withTime_passes)
        //let dateValue = try _decodeDateNew(dateString, format: withTime_fails1)
        //let dateValue = try _decodeDateNew(dateString, format: withTime_fails2)

        func _decodeDateClassic(from value:String, formatString:String = "YYYY-MM-dd' 'HH:mm:ss") throws -> Date {
            let dateFormatter = DateFormatter() //in real code defined at top level.
            dateFormatter.timeZone = .gmt 
            dateFormatter.dateFormat = formatString
            
            guard let date = dateFormatter.date(from: value) else {
                throw DecodingError.dataCorrupted(
                    .init(codingPath: [], debugDescription: "String not in expected \(dateFormatter.dateFormat.description) format.")
                )
            }
            return date
        }
        
        
        let dateValueClassic = try _decodeDateClassic(from: dateString)
        
        XCTAssertEqual(dateValue, dateValueClassic)
    }

Seems like this works: Date.ParseStrategy(format: "'YYYY-MM-dd' 'HH:mm:ss'", timeZone: .gmt). The single-quote at the beginning/end trick it into interpreting your format.

1 Like

Thank you so much. Everything inside the single quotes. I was indeed missing something simple! You saved me so much time.

Interesting quirk - it's letting it parse a date with that string, but it's placing the year in 2023.

Using a single lowercase y like in the one generated by Date.FormatString fixed it.("'y-MM-dd' 'HH:mm:ss'") Y in this context aparently means "year for week of year". I'll have to do some updating depending on if it's the DateFormatter or a ParseStrategy but it is nice to be able to use a regular string.

Thank you again.

(ETA works: "'y'-'MM'-'dd' 'HH':'mm':'ss'", fails: "y'-'MM'-'dd' 'HH':'mm':'ss")
(ETA: When the date string is let dateString = "20240312031210" the string has to be "'yyyyMMddHHmmss'" and the full initializer has to have the year as \(year: .extended(minimumLength: 4)) as .defaultdigits does not work.

Hand-tooled literals do seem to have inconsistent results. I think I would recommend against them if one is in a hurry.

FB13700896

For the record, the code I'd actually recommend for what I'm doing is:

let dateString = "2024-03-12 03:12:10" //(DANGER. No time zone is a questionable choice.)
let isoDateFormat:Date.ISO8601FormatStyle = .iso8601.dateTimeSeparator(.space).year().month().day().time(includingFractionalSeconds: false)
let date = try isoDateFormat.parse(dateString)
print(isoDateFormat2.format(date))

Code that's wonky:


    func testVerbatimFormatStyle() throws {
        let dateString = "2024-03-12"
        var dashString:Date.FormatString { "\(year: .defaultDigits)-\(month: .twoDigits)-\(day: .twoDigits)" }
        
        let verbatimFormat:Date.VerbatimFormatStyle = .init(format: dashString, timeZone: TimeZone.gmt, calendar: Calendar.current)
        let verbatimDate = try verbatimFormat.parseStrategy.parse(dateString)
        let stringFromVDate = verbatimFormat.format(verbatimDate)
        print(verbatimFormat.parseStrategy.format) //SIDENOTE: other FormatStyles have myFormat.parse(String)
        print("date:", verbatimDate, "string:", stringFromVDate)

        var dashString2:Date.FormatString { "'y-MM-dd'" }
        
        //Works with dashString2
        let strategy = Date.ParseStrategy(format:  dashString2, timeZone: .gmt)
        //strategy.format == "\'\'y-MM-dd\'\'"	
        let date = try Date(dateString, strategy: strategy)
        print("newDate:", date)
        //newDate 2024-03-12 00:00:00 +0000
        let formatString = strategy.format //returns the format string.
        print(formatString)
        let date2 = try strategy.parse(dateString)
        
        //Both pass.
        XCTAssertEqual(date, date2)
        XCTAssertEqual(date, verbatimDate)
        
        //Fails with dashString2
        let verbatimFormat2:Date.VerbatimFormatStyle = .init(format: dashString2, timeZone: TimeZone.gmt, calendar: Calendar.current)
        //verbatimFormat2.formatPattern == "\'\'y-MM-dd\'\'"	
        let verbatimDate2 = try verbatimFormat2.parseStrategy.parse(dateString) // <--CAUGHT ERROR HERE.
        let stringFromVDate2 = verbatimFormat2.format(verbatimDate2)
        print(verbatimFormat2.parseStrategy.format)
        print("date:", verbatimDate2, "string:", stringFromVDate2)
        
        XCTAssertEqual(verbatimDate, verbatimDate2) //Never gets here.
    }


//MARK:  Playground Code

// Doesn’t even work in ParseStrategy

import UIKit

let dateString = "2024-03-12"
var dashString:Date.FormatString { "\(year: .defaultDigits)-\(month: .twoDigits)-\(day: .twoDigits)" }

let verbatimFormat:Date.VerbatimFormatStyle = .init(format: dashString, timeZone: TimeZone.gmt, calendar: Calendar.current)
let verbatimDate = try verbatimFormat.parseStrategy.parse(dateString)
let stringFromVDate = verbatimFormat.format(verbatimDate)
print(verbatimFormat.parseStrategy.format) //Sidenote:other FormatStyles have myFormat.parse(String)
print("date:", verbatimDate, "string:", stringFromVDate)

var dashString2:Date.FormatString { "'y-MM-dd'" }

let strategy = Date.ParseStrategy(format:  dashString2, timeZone: .gmt)
let date = try Date(dateString, strategy: strategy)  //<--error here in playground.
//strategy.formatPattern == "'''y-MM-dd'''"
print("newDate:", date)
//newDate 2024-03-12 00:00:00 +0000
let formatString = strategy.format //returns the format string.
print(formatString)
let date2 = try strategy.parse(dateString)

print(date == date2)
print(date == verbatimDate)

let verbatimFormat2:Date.VerbatimFormatStyle = .init(format: dashString2, timeZone: TimeZone.gmt, calendar: Calendar.current)
//verbatimFormat2.formatPattern == "'''y-MM-dd'''"
let verbatimDate2 = try verbatimFormat2.parseStrategy.parse(dateString)
let stringFromVDate2 = verbatimFormat2.format(verbatimDate2)
print(verbatimFormat2.parseStrategy.format)
print("date:", verbatimDate2, "string:", stringFromVDate2)

ETA: Note a successful format round trip for a ParseStrategy looks like

        var dashString3:Date.FormatString {
            Date.FormatString(stringLiteral: "'\(verbatimFormat.parseStrategy.format)'")
        }
        
        let strategy3 = Date.ParseStrategy(format:  dashString3, timeZone: .gmt)
        let date3 = try Date(dateString, strategy: strategy3)

(Still doesn't work for a VerbatimFormatStyle parse.)