Skip to content

Commit 77493e1

Browse files
committed
Update with latest changes
1 parent fecd2a5 commit 77493e1

File tree

6 files changed

+240
-116
lines changed

6 files changed

+240
-116
lines changed

Benchmarks/Benchmarks/Formatting/BenchmarkFormatting.swift

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,40 +45,50 @@ let benchmarks = {
4545

4646
let preformatted = formats.map { ($0, $0.format(date)) }
4747

48-
Benchmark("iso860-format", configuration: .init(scalingFactor: .kilo)) { benchmark in
49-
for fmt in formats {
50-
blackHole(fmt.format(date))
48+
Benchmark("iso8601-format", configuration: .init(scalingFactor: .kilo)) { benchmark in
49+
for _ in benchmark.scaledIterations {
50+
for fmt in formats {
51+
blackHole(fmt.format(date))
52+
}
5153
}
5254
}
5355

54-
Benchmark("iso860-parse", configuration: .init(scalingFactor: .kilo)) { benchmark in
55-
for fmt in preformatted {
56-
let result = try? fmt.0.parse(fmt.1)
57-
blackHole(result)
56+
Benchmark("iso8601-parse", configuration: .init(scalingFactor: .kilo)) { benchmark in
57+
for _ in benchmark.scaledIterations {
58+
for fmt in preformatted {
59+
let result = try? fmt.0.parse(fmt.1)
60+
blackHole(result)
61+
}
5862
}
5963
}
6064

6165
Benchmark("parallel-number-formatting", configuration: .init(scalingFactor: .kilo)) { benchmark in
62-
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
63-
let result = 10.123.formatted()
64-
blackHole(result)
66+
for _ in benchmark.scaledIterations {
67+
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
68+
let result = 10.123.formatted()
69+
blackHole(result)
70+
}
6571
}
6672
}
6773

6874
Benchmark("parallel-and-serialized-number-formatting", configuration: .init(scalingFactor: .kilo)) { benchmark in
69-
DispatchQueue.concurrentPerform(iterations: 10) { _ in
70-
// Reuse the values on this thread a bunch
71-
for _ in 0..<100 {
72-
let result = 10.123.formatted()
73-
blackHole(result)
75+
for _ in benchmark.scaledIterations {
76+
DispatchQueue.concurrentPerform(iterations: 10) { _ in
77+
// Reuse the values on this thread a bunch
78+
for _ in 0..<100 {
79+
let result = 10.123.formatted()
80+
blackHole(result)
81+
}
7482
}
7583
}
7684
}
7785

7886
Benchmark("serialized-number-formatting", configuration: .init(scalingFactor: .kilo)) { benchmark in
79-
for _ in 0..<1000 {
80-
let result = 10.123.formatted()
81-
blackHole(result)
87+
for _ in benchmark.scaledIterations {
88+
for _ in 0..<1000 {
89+
let result = 10.123.formatted()
90+
blackHole(result)
91+
}
8292
}
8393
}
8494

Sources/FoundationEssentials/Formatting/Date+HTTPFormatStyle.swift

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,28 @@ extension RegexComponent where Self == Date.HTTPFormatStyle {
112112
}
113113
}
114114

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+
}
116126

117127
@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()
121132
}
122133
}
123134

135+
// MARK: - Components
136+
124137
@available(FoundationPreview 6.2, *)
125138
public extension FormatStyle where Self == DateComponents.HTTPFormatStyle {
126139
static var http: Self {
@@ -152,6 +165,7 @@ extension DateComponents.HTTPFormatStyle : ParseStrategy {
152165
extension DateComponents {
153166
/// Converts `DateComponents` into RFC 9110-compatible "HTTP date" `String`, and parses in the reverse direction.
154167
/// 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.
155169
public struct HTTPFormatStyle : Sendable, Hashable, Codable, ParseableFormatStyle {
156170
public init() {
157171
}
@@ -164,10 +178,6 @@ extension DateComponents {
164178
var buffer = OutputBuffer(initializing: _buffer.baseAddress!, capacity: _buffer.count)
165179

166180
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")))
171181
case 2:
172182
buffer.appendElement(CChar(UInt8(ascii: "M")))
173183
buffer.appendElement(CChar(UInt8(ascii: "o")))
@@ -192,8 +202,13 @@ extension DateComponents {
192202
buffer.appendElement(CChar(UInt8(ascii: "S")))
193203
buffer.appendElement(CChar(UInt8(ascii: "a")))
194204
buffer.appendElement(CChar(UInt8(ascii: "t")))
205+
case 1:
206+
// Sunday, or default / missing
207+
fallthrough
195208
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")))
197212
}
198213

199214
buffer.appendElement(CChar(UInt8(ascii: ",")))
@@ -204,10 +219,6 @@ extension DateComponents {
204219
buffer.appendElement(CChar(UInt8(ascii: " ")))
205220

206221
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")))
211222
case 2:
212223
buffer.appendElement(CChar(UInt8(ascii: "F")))
213224
buffer.appendElement(CChar(UInt8(ascii: "e")))
@@ -252,18 +263,23 @@ extension DateComponents {
252263
buffer.appendElement(CChar(UInt8(ascii: "D")))
253264
buffer.appendElement(CChar(UInt8(ascii: "e")))
254265
buffer.appendElement(CChar(UInt8(ascii: "c")))
266+
case 1:
267+
// Jan or default value
268+
fallthrough
255269
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")))
257273
}
258274
buffer.appendElement(CChar(UInt8(ascii: " ")))
259275

260276
let year = components.year ?? 2000
261277
buffer.append(year, zeroPad: 4)
262278
buffer.appendElement(CChar(UInt8(ascii: " ")))
263279

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
267283

268284
buffer.append(h, zeroPad: 2)
269285
buffer.appendElement(CChar(UInt8(ascii: ":")))
@@ -335,7 +351,7 @@ extension DateComponents {
335351
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now))
336352
}
337353

338-
if maybeWeekday1 >= UInt8(ascii: "0") && maybeWeekday1 <= UInt8(ascii: "9") {
354+
if isASCIIDigit(maybeWeekday1) {
339355
// This is the first digit of the day. Weekday is not present.
340356
} else {
341357
// Anything else must be a day-name (Mon, Tue, ... Sun)
@@ -363,8 +379,8 @@ extension DateComponents {
363379
}
364380

365381
// 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")
368384
}
369385

370386
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 {
424440

425441
try it.expectCharacter(UInt8(ascii: ":"), input: inputString, onFailure: Date.HTTPFormatStyle().format(Date.now))
426442
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.
428444
if second < 0 || second > 60 {
429445
throw parseError(inputString, exampleFormattedString: Date.HTTPFormatStyle().format(Date.now), extendedDescription: "Second \(second) is out of bounds")
430446
}
447+
// Foundation does not support leap seconds. We convert 60 seconds into 59 seconds.
431448
if second == 60 {
432-
dc.second = 0
449+
dc.second = 59
433450
} else {
434451
dc.second = second
435452
}

Sources/FoundationEssentials/Formatting/Date+ISO8601FormatStyle.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ extension RegexComponent where Self == Date.ISO8601FormatStyle {
348348
/// - Parameters:
349349
/// - timeZone: The time zone to create the captured `Date` with.
350350
/// - dateSeparator: The separator between date components.
351-
/// - Returns: A `RegexComponent` to match an ISO 8601 date string, including time zone.
351+
/// - Returns: A `RegexComponent` to match an ISO 8601 date string, not any time zone that may be in the string.
352352
public static func iso8601Date(timeZone: TimeZone, dateSeparator: Self.DateSeparator = .dash) -> Self {
353353
return Date.ISO8601FormatStyle(dateSeparator: dateSeparator, timeZone: timeZone).year().month().day()
354354
}

Sources/FoundationEssentials/Formatting/DateComponents+ISO8601FormatStyle.swift

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,7 @@ extension DateComponents {
4545
}
4646
}
4747

48-
internal private(set) var _formatFields: Fields = []
49-
50-
/// This is a cache of the Gregorian Calendar, updated if the time zone changes.
51-
/// In the future we can eliminate this by moving the calculations for the gregorian calendar into static functions there.
52-
internal private(set) var _calendar: Calendar
53-
54-
private mutating func insertFormatFields(_ fields: Fields) {
55-
_formatFields.insert(fields)
56-
}
57-
48+
private var _formatFields: Fields = []
5849
// Used from Date.ISO8601FormatStyle's format
5950
internal var formatFields: Fields {
6051
if _formatFields.isEmpty {
@@ -63,6 +54,14 @@ extension DateComponents {
6354
return _formatFields
6455
}
6556
}
57+
58+
/// This is a cache of the Gregorian Calendar, updated if the time zone changes.
59+
/// In the future we can eliminate this by moving the calculations for the gregorian calendar into static functions there.
60+
internal private(set) var _calendar: Calendar
61+
62+
private mutating func insertFormatFields(_ fields: Fields) {
63+
_formatFields.insert(fields)
64+
}
6665

6766
enum CodingKeys : String, CodingKey {
6867
case timeZoneSeparator
@@ -408,7 +407,7 @@ extension DateComponents.ISO8601FormatStyle {
408407
var components: DateComponents
409408
}
410409

411-
private func components(from inputString: String, fillMissingUnits: Bool, in view: borrowing BufferView<UInt8>) throws -> ComponentsParseResult {
410+
private func components(from inputString: String, fillMissingUnits: Bool, defaultTimeZone: TimeZone, in view: borrowing BufferView<UInt8>) throws -> ComponentsParseResult {
412411
let fields = formatFields
413412

414413
var it = view.makeIterator()
@@ -426,7 +425,7 @@ extension DateComponents.ISO8601FormatStyle {
426425
var minute: Int?
427426
var second: Int?
428427
var nanosecond: Int?
429-
var timeZone: TimeZone?
428+
var timeZone = defaultTimeZone
430429

431430
if fields.contains(.year) {
432431
let max = dateSeparator == .omitted ? 4 : nil
@@ -702,7 +701,7 @@ extension DateComponents.ISO8601FormatStyle {
702701

703702
@available(FoundationPreview 6.2, *)
704703
public extension FormatStyle where Self == DateComponents.ISO8601FormatStyle {
705-
static var iso8601Components: Self {
704+
static var iso8601: Self {
706705
return DateComponents.ISO8601FormatStyle()
707706
}
708707
}
@@ -713,23 +712,23 @@ public extension FormatStyle where Self == DateComponents.ISO8601FormatStyle {
713712

714713
@available(FoundationPreview 6.2, *)
715714
public extension ParseableFormatStyle where Self == DateComponents.ISO8601FormatStyle {
716-
static var iso8601Components: Self { .init() }
715+
static var iso8601: Self { .init() }
717716
}
718717

719718
@available(FoundationPreview 6.2, *)
720719
public extension ParseStrategy where Self == DateComponents.ISO8601FormatStyle {
721720
@_disfavoredOverload
722-
static var iso8601Components: Self { .init() }
721+
static var iso8601: Self { .init() }
723722
}
724723

725724

726725
@available(FoundationPreview 6.2, *)
727726
extension DateComponents.ISO8601FormatStyle : ParseStrategy {
728727
public func parse(_ value: String) throws -> DateComponents {
729-
guard let (_, date) = parse(value, fillMissingUnits: false, in: value.startIndex..<value.endIndex) else {
728+
guard let (_, components) = parse(value, fillMissingUnits: false, in: value.startIndex..<value.endIndex) else {
730729
throw parseError(value, exampleFormattedString: Date.ISO8601FormatStyle(self).format(Date.now))
731730
}
732-
return date
731+
return components
733732
}
734733

735734
internal func parse(_ value: String, fillMissingUnits: Bool, in range: Range<String.Index>) -> (String.Index, DateComponents)? {
@@ -741,7 +740,7 @@ extension DateComponents.ISO8601FormatStyle : ParseStrategy {
741740
let result = v.withUTF8 { buffer -> (Int, DateComponents)? in
742741
let view = BufferView(unsafeBufferPointer: buffer)!
743742

744-
guard let comps = try? components(from: value, fillMissingUnits: fillMissingUnits, in: view) else {
743+
guard let comps = try? components(from: value, fillMissingUnits: fillMissingUnits, defaultTimeZone: timeZone, in: view) else {
745744
return nil
746745
}
747746

@@ -814,8 +813,8 @@ extension RegexComponent where Self == DateComponents.ISO8601FormatStyle {
814813
/// - Parameters:
815814
/// - timeZone: The time zone to create the captured `Date` with.
816815
/// - dateSeparator: The separator between date components.
817-
/// - Returns: A `RegexComponent` to match an ISO 8601 date string, including time zone.
818-
public static func iso8601Components(timeZone: TimeZone, dateSeparator: Date.ISO8601FormatStyle.DateSeparator = .dash) -> Self {
816+
/// - Returns: A `RegexComponent` to match an ISO 8601 date string, not any time zone that may be in the string.
817+
public static func iso8601DateComponents(timeZone: TimeZone, dateSeparator: Date.ISO8601FormatStyle.DateSeparator = .dash) -> Self {
819818
return DateComponents.ISO8601FormatStyle(dateSeparator: dateSeparator, timeZone: timeZone).year().month().day()
820819
}
821820
}

Tests/FoundationEssentialsTests/Formatting/ISO8601FormatStyleFormattingTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,12 @@ final class ISO8601FormatStyleFormattingTests: XCTestCase {
8383
func test_ISO8601ComponentsFormatMissingPieces() throws {
8484
// Example code from the proposal
8585
let components = DateComponents(year: 1999, month: 12, day: 31)
86-
let formatted = components.formatted(.iso8601Components)
86+
let formatted = components.formatted(.iso8601)
8787
XCTAssertEqual(formatted, "1999-12-31T00:00:00Z")
8888

8989

9090
let emptyComponents = DateComponents()
91-
let emptyFormatted = emptyComponents.formatted(.iso8601Components)
91+
let emptyFormatted = emptyComponents.formatted(.iso8601)
9292
XCTAssertEqual(emptyFormatted, "1970-01-01T00:00:00Z")
9393
}
9494

0 commit comments

Comments
 (0)