Skip to content

Commit 8d06683

Browse files
parkeratheMomax
authored andcommitted
ISO8601 DateComponents format style (#1209)
* ISO8601 DateComponents style * Add Hashable to ISO8601FormatStyle
1 parent a9fb323 commit 8d06683

13 files changed

+1366
-661
lines changed

Benchmarks/Benchmarks/Formatting/BenchmarkFormatting.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,15 @@ let benchmarks = {
4545

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

48-
Benchmark("iso860-format", configuration: .init(scalingFactor: .kilo)) { benchmark in
48+
Benchmark("iso8601-format", configuration: .init(scalingFactor: .kilo)) { benchmark in
4949
for _ in benchmark.scaledIterations {
5050
for fmt in formats {
5151
blackHole(fmt.format(date))
5252
}
5353
}
5454
}
5555

56-
Benchmark("iso860-parse", configuration: .init(scalingFactor: .kilo)) { benchmark in
56+
Benchmark("iso8601-parse", configuration: .init(scalingFactor: .kilo)) { benchmark in
5757
for _ in benchmark.scaledIterations {
5858
for fmt in preformatted {
5959
let result = try? fmt.0.parse(fmt.1)

Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2073,36 +2073,49 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
20732073
isLeapYear = false
20742074
}
20752075

2076-
var dc = DateComponents()
2077-
if components.contains(.calendar) {
2078-
var calendar = Calendar(identifier: .gregorian)
2079-
calendar.timeZone = timeZone
2080-
dc.calendar = calendar
2081-
}
2082-
if components.contains(.timeZone) { dc.timeZone = timeZone }
2076+
var dcCalendar: Calendar?
2077+
var dcTimeZone: TimeZone?
2078+
var dcEra: Int?
2079+
var dcYear: Int?
2080+
var dcMonth: Int?
2081+
var dcDay: Int?
2082+
var dcDayOfYear: Int?
2083+
var dcHour: Int?
2084+
var dcMinute: Int?
2085+
var dcSecond: Int?
2086+
var dcWeekday: Int?
2087+
var dcWeekdayOrdinal: Int?
2088+
var dcQuarter: Int?
2089+
var dcWeekOfMonth: Int?
2090+
var dcWeekOfYear: Int?
2091+
var dcYearForWeekOfYear: Int?
2092+
var dcNanosecond: Int?
2093+
var dcIsLeapMonth: Bool?
2094+
2095+
// DateComponents sets the time zone on the calendar if appropriate
2096+
if components.contains(.calendar) { dcCalendar = Calendar(identifier: identifier) }
2097+
if components.contains(.timeZone) { dcTimeZone = timeZone }
20832098
if components.contains(.era) {
2084-
let era: Int
20852099
if year < 1 {
2086-
era = 0
2100+
dcEra = 0
20872101
} else {
2088-
era = 1
2102+
dcEra = 1
20892103
}
2090-
dc.era = era
20912104
}
20922105
if components.contains(.year) {
20932106
if year < 1 {
20942107
year = 1 - year
20952108
}
2096-
dc.year = year
2109+
dcYear = year
20972110
}
2098-
if components.contains(.month) { dc.month = month }
2099-
if components.contains(.day) { dc.day = day }
2100-
if components.contains(.dayOfYear) { dc.dayOfYear = dayOfYear }
2101-
if components.contains(.hour) { dc.hour = hour }
2102-
if components.contains(.minute) { dc.minute = minute }
2103-
if components.contains(.second) { dc.second = second }
2104-
if components.contains(.weekday) { dc.weekday = weekday }
2105-
if components.contains(.weekdayOrdinal) { dc.weekdayOrdinal = weekdayOrdinal }
2111+
if components.contains(.month) { dcMonth = month }
2112+
if components.contains(.day) { dcDay = day }
2113+
if components.contains(.dayOfYear) { dcDayOfYear = dayOfYear }
2114+
if components.contains(.hour) { dcHour = hour }
2115+
if components.contains(.minute) { dcMinute = minute }
2116+
if components.contains(.second) { dcSecond = second }
2117+
if components.contains(.weekday) { dcWeekday = weekday }
2118+
if components.contains(.weekdayOrdinal) { dcWeekdayOrdinal = weekdayOrdinal }
21062119
if components.contains(.quarter) {
21072120
let quarter = if !isLeapYear {
21082121
if dayOfYear < 90 { 1 }
@@ -2118,15 +2131,16 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
21182131
else { fatalError() }
21192132
}
21202133

2121-
dc.quarter = quarter
2134+
dcQuarter = quarter
21222135
}
2123-
if components.contains(.weekOfMonth) { dc.weekOfMonth = weekOfMonth }
2124-
if components.contains(.weekOfYear) { dc.weekOfYear = weekOfYear }
2125-
if components.contains(.yearForWeekOfYear) { dc.yearForWeekOfYear = yearForWeekOfYear }
2126-
if components.contains(.nanosecond) { dc.nanosecond = nanosecond }
2136+
if components.contains(.weekOfMonth) { dcWeekOfMonth = weekOfMonth }
2137+
if components.contains(.weekOfYear) { dcWeekOfYear = weekOfYear }
2138+
if components.contains(.yearForWeekOfYear) { dcYearForWeekOfYear = yearForWeekOfYear }
2139+
if components.contains(.nanosecond) { dcNanosecond = nanosecond }
21272140

2128-
if components.contains(.isLeapMonth) || components.contains(.month) { dc.isLeapMonth = false }
2129-
return dc
2141+
if components.contains(.isLeapMonth) || components.contains(.month) { dcIsLeapMonth = false }
2142+
2143+
return DateComponents(calendar: dcCalendar, timeZone: dcTimeZone, rawEra: dcEra, rawYear: dcYear, rawMonth: dcMonth, rawDay: dcDay, rawHour: dcHour, rawMinute: dcMinute, rawSecond: dcSecond, rawNanosecond: dcNanosecond, rawWeekday: dcWeekday, rawWeekdayOrdinal: dcWeekdayOrdinal, rawQuarter: dcQuarter, rawWeekOfMonth: dcWeekOfMonth, rawWeekOfYear: dcWeekOfYear, rawYearForWeekOfYear: dcYearForWeekOfYear, rawDayOfYear: dcDayOfYear, isLeapMonth: dcIsLeapMonth)
21302144
}
21312145

21322146
func dateComponents(_ components: Calendar.ComponentSet, from date: Date) -> DateComponents {

Sources/FoundationEssentials/Calendar/DateComponents.swift

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,56 @@ public struct DateComponents : Hashable, Equatable, Sendable {
7575
self.yearForWeekOfYear = yearForWeekOfYear
7676
self.dayOfYear = nil
7777
}
78+
79+
/// Same as the public initializer, but with the dayOfYear field, and skipping the 'conversion' for callers who expect ObjC behavior (Int.max -> nil).
80+
@inline(__always)
81+
internal init(calendar: Calendar? = nil,
82+
timeZone: TimeZone? = nil,
83+
rawEra: Int? = nil,
84+
rawYear: Int? = nil,
85+
rawMonth: Int? = nil,
86+
rawDay: Int? = nil,
87+
rawHour: Int? = nil,
88+
rawMinute: Int? = nil,
89+
rawSecond: Int? = nil,
90+
rawNanosecond: Int? = nil,
91+
rawWeekday: Int? = nil,
92+
rawWeekdayOrdinal: Int? = nil,
93+
rawQuarter: Int? = nil,
94+
rawWeekOfMonth: Int? = nil,
95+
rawWeekOfYear: Int? = nil,
96+
rawYearForWeekOfYear: Int? = nil,
97+
rawDayOfYear: Int? = nil,
98+
isLeapMonth: Bool? = nil) {
99+
100+
// Be sure to set the time zone of the calendar if appropriate
101+
if var calendar, let timeZone {
102+
calendar.timeZone = timeZone
103+
_calendar = calendar
104+
_timeZone = timeZone
105+
} else if let calendar {
106+
_calendar = calendar
107+
} else if let timeZone {
108+
_timeZone = timeZone
109+
}
110+
111+
_era = rawEra
112+
_year = rawYear
113+
_month = rawMonth
114+
_day = rawDay
115+
_hour = rawHour
116+
_minute = rawMinute
117+
_second = rawSecond
118+
_nanosecond = rawNanosecond
119+
_weekday = rawWeekday
120+
_weekdayOrdinal = rawWeekdayOrdinal
121+
_quarter = rawQuarter
122+
_weekOfMonth = rawWeekOfMonth
123+
_weekOfYear = rawWeekOfYear
124+
_yearForWeekOfYear = rawYearForWeekOfYear
125+
_dayOfYear = rawDayOfYear
126+
_isLeapMonth = isLeapMonth
127+
}
78128

79129
package init?(component: Calendar.Component, value: Int) {
80130
switch component {
@@ -116,10 +166,12 @@ public struct DateComponents : Hashable, Equatable, Sendable {
116166
public var timeZone: TimeZone? {
117167
get { _timeZone }
118168
set {
119-
_timeZone = newValue
120-
// Also changes the time zone of the calendar
121-
if let newValue {
122-
_calendar?.timeZone = newValue
169+
if _timeZone != newValue {
170+
_timeZone = newValue
171+
// Also changes the time zone of the calendar
172+
if let newValue {
173+
_calendar?.timeZone = newValue
174+
}
123175
}
124176
}
125177
}

Sources/FoundationEssentials/Formatting/Date+HTTPFormatStyle.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,26 @@ extension RegexComponent where Self == Date.HTTPFormatStyle {
112112
}
113113
}
114114

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+
}
126+
127+
@available(FoundationPreview 6.2, *)
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()
132+
}
133+
}
134+
115135
// MARK: - Components
116136

117137
@available(FoundationPreview 6.2, *)

0 commit comments

Comments
 (0)