Skip to content

ISO8601 DateComponents format style #1209

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Benchmarks/Benchmarks/Formatting/BenchmarkFormatting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ let benchmarks = {

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

Benchmark("iso860-format", configuration: .init(scalingFactor: .kilo)) { benchmark in
Benchmark("iso8601-format", configuration: .init(scalingFactor: .kilo)) { benchmark in
for _ in benchmark.scaledIterations {
for fmt in formats {
blackHole(fmt.format(date))
}
}
}

Benchmark("iso860-parse", configuration: .init(scalingFactor: .kilo)) { benchmark in
Benchmark("iso8601-parse", configuration: .init(scalingFactor: .kilo)) { benchmark in
for _ in benchmark.scaledIterations {
for fmt in preformatted {
let result = try? fmt.0.parse(fmt.1)
Expand Down
68 changes: 41 additions & 27 deletions Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2073,36 +2073,49 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
isLeapYear = false
}

var dc = DateComponents()
if components.contains(.calendar) {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = timeZone
dc.calendar = calendar
}
if components.contains(.timeZone) { dc.timeZone = timeZone }
var dcCalendar: Calendar?
var dcTimeZone: TimeZone?
var dcEra: Int?
var dcYear: Int?
var dcMonth: Int?
var dcDay: Int?
var dcDayOfYear: Int?
var dcHour: Int?
var dcMinute: Int?
var dcSecond: Int?
var dcWeekday: Int?
var dcWeekdayOrdinal: Int?
var dcQuarter: Int?
var dcWeekOfMonth: Int?
var dcWeekOfYear: Int?
var dcYearForWeekOfYear: Int?
var dcNanosecond: Int?
var dcIsLeapMonth: Bool?

// DateComponents sets the time zone on the calendar if appropriate
if components.contains(.calendar) { dcCalendar = Calendar(identifier: identifier) }
if components.contains(.timeZone) { dcTimeZone = timeZone }
if components.contains(.era) {
let era: Int
if year < 1 {
era = 0
dcEra = 0
} else {
era = 1
dcEra = 1
}
dc.era = era
}
if components.contains(.year) {
if year < 1 {
year = 1 - year
}
dc.year = year
dcYear = year
}
if components.contains(.month) { dc.month = month }
if components.contains(.day) { dc.day = day }
if components.contains(.dayOfYear) { dc.dayOfYear = dayOfYear }
if components.contains(.hour) { dc.hour = hour }
if components.contains(.minute) { dc.minute = minute }
if components.contains(.second) { dc.second = second }
if components.contains(.weekday) { dc.weekday = weekday }
if components.contains(.weekdayOrdinal) { dc.weekdayOrdinal = weekdayOrdinal }
if components.contains(.month) { dcMonth = month }
if components.contains(.day) { dcDay = day }
if components.contains(.dayOfYear) { dcDayOfYear = dayOfYear }
if components.contains(.hour) { dcHour = hour }
if components.contains(.minute) { dcMinute = minute }
if components.contains(.second) { dcSecond = second }
if components.contains(.weekday) { dcWeekday = weekday }
if components.contains(.weekdayOrdinal) { dcWeekdayOrdinal = weekdayOrdinal }
if components.contains(.quarter) {
let quarter = if !isLeapYear {
if dayOfYear < 90 { 1 }
Expand All @@ -2118,15 +2131,16 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
else { fatalError() }
}

dc.quarter = quarter
dcQuarter = quarter
}
if components.contains(.weekOfMonth) { dc.weekOfMonth = weekOfMonth }
if components.contains(.weekOfYear) { dc.weekOfYear = weekOfYear }
if components.contains(.yearForWeekOfYear) { dc.yearForWeekOfYear = yearForWeekOfYear }
if components.contains(.nanosecond) { dc.nanosecond = nanosecond }
if components.contains(.weekOfMonth) { dcWeekOfMonth = weekOfMonth }
if components.contains(.weekOfYear) { dcWeekOfYear = weekOfYear }
if components.contains(.yearForWeekOfYear) { dcYearForWeekOfYear = yearForWeekOfYear }
if components.contains(.nanosecond) { dcNanosecond = nanosecond }

if components.contains(.isLeapMonth) || components.contains(.month) { dc.isLeapMonth = false }
return dc
if components.contains(.isLeapMonth) || components.contains(.month) { dcIsLeapMonth = false }

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)
}

func dateComponents(_ components: Calendar.ComponentSet, from date: Date) -> DateComponents {
Expand Down
60 changes: 56 additions & 4 deletions Sources/FoundationEssentials/Calendar/DateComponents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,56 @@ public struct DateComponents : Hashable, Equatable, Sendable {
self.yearForWeekOfYear = yearForWeekOfYear
self.dayOfYear = nil
}

/// Same as the public initializer, but with the dayOfYear field, and skipping the 'conversion' for callers who expect ObjC behavior (Int.max -> nil).
@inline(__always)
internal init(calendar: Calendar? = nil,
timeZone: TimeZone? = nil,
rawEra: Int? = nil,
rawYear: Int? = nil,
rawMonth: Int? = nil,
rawDay: Int? = nil,
rawHour: Int? = nil,
rawMinute: Int? = nil,
rawSecond: Int? = nil,
rawNanosecond: Int? = nil,
rawWeekday: Int? = nil,
rawWeekdayOrdinal: Int? = nil,
rawQuarter: Int? = nil,
rawWeekOfMonth: Int? = nil,
rawWeekOfYear: Int? = nil,
rawYearForWeekOfYear: Int? = nil,
rawDayOfYear: Int? = nil,
isLeapMonth: Bool? = nil) {

// Be sure to set the time zone of the calendar if appropriate
if var calendar, let timeZone {
calendar.timeZone = timeZone
_calendar = calendar
_timeZone = timeZone
} else if let calendar {
_calendar = calendar
} else if let timeZone {
_timeZone = timeZone
}

_era = rawEra
_year = rawYear
_month = rawMonth
_day = rawDay
_hour = rawHour
_minute = rawMinute
_second = rawSecond
_nanosecond = rawNanosecond
_weekday = rawWeekday
_weekdayOrdinal = rawWeekdayOrdinal
_quarter = rawQuarter
_weekOfMonth = rawWeekOfMonth
_weekOfYear = rawWeekOfYear
_yearForWeekOfYear = rawYearForWeekOfYear
_dayOfYear = rawDayOfYear
_isLeapMonth = isLeapMonth
}

package init?(component: Calendar.Component, value: Int) {
switch component {
Expand Down Expand Up @@ -116,10 +166,12 @@ public struct DateComponents : Hashable, Equatable, Sendable {
public var timeZone: TimeZone? {
get { _timeZone }
set {
_timeZone = newValue
// Also changes the time zone of the calendar
if let newValue {
_calendar?.timeZone = newValue
if _timeZone != newValue {
_timeZone = newValue
// Also changes the time zone of the calendar
if let newValue {
_calendar?.timeZone = newValue
}
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions Sources/FoundationEssentials/Formatting/Date+HTTPFormatStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,26 @@ extension RegexComponent where Self == Date.HTTPFormatStyle {
}
}

@available(FoundationPreview 6.2, *)
extension DateComponents.HTTPFormatStyle : CustomConsumingRegexComponent {
public typealias RegexOutput = DateComponents
public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range<String.Index>) throws -> (upperBound: String.Index, output: DateComponents)? {
guard index < bounds.upperBound else {
return nil
}
// It's important to return nil from parse in case of a failure, not throw. That allows things like the firstMatch regex to work.
return self.parse(input, in: index..<bounds.upperBound)
}
}

@available(FoundationPreview 6.2, *)
extension RegexComponent where Self == DateComponents.HTTPFormatStyle {
/// 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.
public static var httpComponents: DateComponents.HTTPFormatStyle {
return DateComponents.HTTPFormatStyle()
}
}

// MARK: - Components

@available(FoundationPreview 6.2, *)
Expand Down
Loading