@@ -112,15 +112,28 @@ extension RegexComponent where Self == Date.HTTPFormatStyle {
112
112
}
113
113
}
114
114
115
- // MARK: - Components
115
+ @available ( FoundationPreview 6 . 2 , * )
116
+ extension DateComponents . HTTPFormatStyle : CustomConsumingRegexComponent {
117
+ public typealias RegexOutput = DateComponents
118
+ public func consuming( _ input: String , startingAt index: String . Index , in bounds: Range < String . Index > ) throws -> ( upperBound: String . Index , output: DateComponents ) ? {
119
+ guard index < bounds. upperBound else {
120
+ return nil
121
+ }
122
+ // It's important to return nil from parse in case of a failure, not throw. That allows things like the firstMatch regex to work.
123
+ return self . parse ( input, in: index..< bounds. upperBound)
124
+ }
125
+ }
116
126
117
127
@available ( FoundationPreview 6 . 2 , * )
118
- extension DateComponents {
119
- public func HTTPComponentsFormat( _ style: HTTPFormatStyle = . init( ) ) -> String {
120
- return style. format ( self )
128
+ extension RegexComponent where Self == DateComponents . HTTPFormatStyle {
129
+ /// Creates a regex component to match an HTTP date and time, such as "2015-11-14'T'15:05:03'Z'", and capture the string as a `DateComponents` using the time zone as specified in the string.
130
+ public static var httpComponents : DateComponents . HTTPFormatStyle {
131
+ return DateComponents . HTTPFormatStyle ( )
121
132
}
122
133
}
123
134
135
+ // MARK: - Components
136
+
124
137
@available ( FoundationPreview 6 . 2 , * )
125
138
public extension FormatStyle where Self == DateComponents . HTTPFormatStyle {
126
139
static var http : Self {
@@ -152,6 +165,7 @@ extension DateComponents.HTTPFormatStyle : ParseStrategy {
152
165
extension DateComponents {
153
166
/// Converts `DateComponents` into RFC 9110-compatible "HTTP date" `String`, and parses in the reverse direction.
154
167
/// This parser does not do validation on the individual values of the components. An optional date can be created from the result using `Calendar(identifier: .gregorian).date(from: ...)`.
168
+ /// When formatting, missing or invalid fields are filled with default values: `Sun`, `01`, `Jan`, `2000`, `00:00:00`, `GMT`. Note that missing fields may result in an invalid date or time. Other values in the `DateComponents` are ignored.
155
169
public struct HTTPFormatStyle : Sendable , Hashable , Codable , ParseableFormatStyle {
156
170
public init ( ) {
157
171
}
@@ -164,10 +178,6 @@ extension DateComponents {
164
178
var buffer = OutputBuffer ( initializing: _buffer. baseAddress!, capacity: _buffer. count)
165
179
166
180
switch components. weekday {
167
- case 1 :
168
- buffer. appendElement ( CChar ( UInt8 ( ascii: " S " ) ) )
169
- buffer. appendElement ( CChar ( UInt8 ( ascii: " u " ) ) )
170
- buffer. appendElement ( CChar ( UInt8 ( ascii: " n " ) ) )
171
181
case 2 :
172
182
buffer. appendElement ( CChar ( UInt8 ( ascii: " M " ) ) )
173
183
buffer. appendElement ( CChar ( UInt8 ( ascii: " o " ) ) )
@@ -192,8 +202,13 @@ extension DateComponents {
192
202
buffer. appendElement ( CChar ( UInt8 ( ascii: " S " ) ) )
193
203
buffer. appendElement ( CChar ( UInt8 ( ascii: " a " ) ) )
194
204
buffer. appendElement ( CChar ( UInt8 ( ascii: " t " ) ) )
205
+ case 1 :
206
+ // Sunday, or default / missing
207
+ fallthrough
195
208
default :
196
- preconditionFailure ( " Invalid weekday \( String ( describing: components. weekday) ) " )
209
+ buffer. appendElement ( CChar ( UInt8 ( ascii: " S " ) ) )
210
+ buffer. appendElement ( CChar ( UInt8 ( ascii: " u " ) ) )
211
+ buffer. appendElement ( CChar ( UInt8 ( ascii: " n " ) ) )
197
212
}
198
213
199
214
buffer. appendElement ( CChar ( UInt8 ( ascii: " , " ) ) )
@@ -204,10 +219,6 @@ extension DateComponents {
204
219
buffer. appendElement ( CChar ( UInt8 ( ascii: " " ) ) )
205
220
206
221
switch components. month {
207
- case 1 :
208
- buffer. appendElement ( CChar ( UInt8 ( ascii: " J " ) ) )
209
- buffer. appendElement ( CChar ( UInt8 ( ascii: " a " ) ) )
210
- buffer. appendElement ( CChar ( UInt8 ( ascii: " n " ) ) )
211
222
case 2 :
212
223
buffer. appendElement ( CChar ( UInt8 ( ascii: " F " ) ) )
213
224
buffer. appendElement ( CChar ( UInt8 ( ascii: " e " ) ) )
@@ -252,18 +263,23 @@ extension DateComponents {
252
263
buffer. appendElement ( CChar ( UInt8 ( ascii: " D " ) ) )
253
264
buffer. appendElement ( CChar ( UInt8 ( ascii: " e " ) ) )
254
265
buffer. appendElement ( CChar ( UInt8 ( ascii: " c " ) ) )
266
+ case 1 :
267
+ // Jan or default value
268
+ fallthrough
255
269
default :
256
- preconditionFailure ( " Invalid month \( String ( describing: components. month) ) " )
270
+ buffer. appendElement ( CChar ( UInt8 ( ascii: " J " ) ) )
271
+ buffer. appendElement ( CChar ( UInt8 ( ascii: " a " ) ) )
272
+ buffer. appendElement ( CChar ( UInt8 ( ascii: " n " ) ) )
257
273
}
258
274
buffer. appendElement ( CChar ( UInt8 ( ascii: " " ) ) )
259
275
260
276
let year = components. year ?? 2000
261
277
buffer. append ( year, zeroPad: 4 )
262
278
buffer. appendElement ( CChar ( UInt8 ( ascii: " " ) ) )
263
279
264
- let h = components. hour!
265
- let m = components. minute!
266
- let s = components. second!
280
+ let h = components. hour ?? 0
281
+ let m = components. minute ?? 0
282
+ let s = components. second ?? 0
267
283
268
284
buffer. append ( h, zeroPad: 2 )
269
285
buffer. appendElement ( CChar ( UInt8 ( ascii: " : " ) ) )
@@ -335,7 +351,7 @@ extension DateComponents {
335
351
throw parseError ( inputString, exampleFormattedString: Date . HTTPFormatStyle ( ) . format ( Date . now) )
336
352
}
337
353
338
- if maybeWeekday1 >= UInt8 ( ascii : " 0 " ) && maybeWeekday1 <= UInt8 ( ascii : " 9 " ) {
354
+ if isASCIIDigit ( maybeWeekday1) {
339
355
// This is the first digit of the day. Weekday is not present.
340
356
} else {
341
357
// Anything else must be a day-name (Mon, Tue, ... Sun)
@@ -363,8 +379,8 @@ extension DateComponents {
363
379
}
364
380
365
381
// Move past , and space to weekday
366
- try it. expectCharacter ( UInt8 ( ascii: " , " ) , input: inputString, onFailure: Date . HTTPFormatStyle ( ) . format ( Date . now) )
367
- try it. expectCharacter ( UInt8 ( ascii: " " ) , input: inputString, onFailure: Date . HTTPFormatStyle ( ) . format ( Date . now) )
382
+ try it. expectCharacter ( UInt8 ( ascii: " , " ) , input: inputString, onFailure: Date . HTTPFormatStyle ( ) . format ( Date . now) , extendedDescription : " Missing , after weekday " )
383
+ try it. expectCharacter ( UInt8 ( ascii: " " ) , input: inputString, onFailure: Date . HTTPFormatStyle ( ) . format ( Date . now) , extendedDescription : " Missing space after weekday " )
368
384
}
369
385
370
386
dc. day = try it. digits ( minDigits: 2 , maxDigits: 2 , input: inputString, onFailure: Date . HTTPFormatStyle ( ) . format ( Date . now) , extendedDescription: " Missing or malformed day " )
@@ -424,12 +440,13 @@ extension DateComponents {
424
440
425
441
try it. expectCharacter ( UInt8 ( ascii: " : " ) , input: inputString, onFailure: Date . HTTPFormatStyle ( ) . format ( Date . now) )
426
442
let second = try it. digits ( minDigits: 2 , maxDigits: 2 , input: inputString, onFailure: Date . HTTPFormatStyle ( ) . format ( Date . now) )
427
- // second '60' is supported in the spec for leap seconds, but Foundation does not support leap seconds. 60 is adjusted to 0 .
443
+ // second '60' is supported in the spec for leap seconds, but Foundation does not support leap seconds. 60 is adjusted to 59 .
428
444
if second < 0 || second > 60 {
429
445
throw parseError ( inputString, exampleFormattedString: Date . HTTPFormatStyle ( ) . format ( Date . now) , extendedDescription: " Second \( second) is out of bounds " )
430
446
}
447
+ // Foundation does not support leap seconds. We convert 60 seconds into 59 seconds.
431
448
if second == 60 {
432
- dc. second = 0
449
+ dc. second = 59
433
450
} else {
434
451
dc. second = second
435
452
}
0 commit comments