Skip to content

Commit d512200

Browse files
authored
Only calculate needed values for _CalendarGregorian.date(_:from:) (swiftlang#1696)
The existing `_CalendarGregorian.date(_:from:)` implementation unconditionally calculates week-related fields regardless what the passed-in `DateComponents` contains. We should not do that unless they are requested. 166835028
1 parent 6e2ef0a commit d512200

2 files changed

Lines changed: 199 additions & 82 deletions

File tree

Benchmarks/Benchmarks/Internationalization/BenchmarkCalendar.swift

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func calendarBenchmarks() {
3838
let cal = Calendar(identifier: .gregorian)
3939
let currentCalendar = Calendar.current
4040
let thanksgivingStart = Date(timeIntervalSince1970: 1474666555.0) //2016-09-23T14:35:55-0700
41-
41+
4242
Benchmark("nextThousandThursdaysInTheFourthWeekOfNovember") { benchmark in
4343
// This benchmark used to be nextThousandThanksgivings, but the name was deceiving since it does not compute the next thousand thanksgivings
4444
let components = DateComponents(month: 11, weekday: 5, weekOfMonth: 4)
@@ -135,7 +135,6 @@ func calendarBenchmarks() {
135135
}
136136
}
137137
}
138-
139138
let testDates = {
140139
let date = Date(timeIntervalSince1970: 0)
141140
var dates = [Date]()
@@ -251,5 +250,62 @@ func calendarBenchmarks() {
251250
assert(id3.isEmpty == false)
252251
}
253252
}
253+
254+
// MARK: - GregorianCalendar
255+
256+
var gmtCalendr = Calendar(identifier: .gregorian)
257+
guard let gmtTimeZone = TimeZone(secondsFromGMT: 0) else {
258+
preconditionFailure("Unexpected nil TimeZone")
259+
}
260+
gmtCalendr.timeZone = gmtTimeZone // use gmt-based time zone so the result doesn't get overshadowed by TimeZone API
261+
262+
Benchmark("GregorianCalendar-dateComponents-yearMonthBasedComponents", configuration: .init(scalingFactor: .mega)) { benchmark in
263+
for date in testDates {
264+
let dc = gmtCalendr.dateComponents([.era, .year, .month, .day, .hour, .minute, .second, .nanosecond], from: date)
265+
blackHole(dc)
266+
}
267+
}
268+
269+
Benchmark("GregorianCalendar-dateComponents-calendarDayComparison", configuration: .init(scalingFactor: .mega)) { benchmark in
270+
for date in testDates {
271+
let dc = gmtCalendr.dateComponents([.year, .month, .day], from: date)
272+
blackHole(dc)
273+
}
274+
}
275+
276+
Benchmark("GregorianCalendar-dateComponents-timestamps", configuration: .init(scalingFactor: .mega)) { benchmark in
277+
for date in testDates {
278+
let dc = gmtCalendr.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date)
279+
blackHole(dc)
280+
}
281+
}
282+
283+
Benchmark("GregorianCalendar-dateComponents-timeValidation", configuration: .init(scalingFactor: .mega)) { benchmark in
284+
for date in testDates {
285+
let dc = gmtCalendr.dateComponents([.hour, .minute, .second], from: date)
286+
blackHole(dc)
287+
}
288+
}
289+
290+
Benchmark("GregorianCalendar-dateComponents-anniversary", configuration: .init(scalingFactor: .mega)) { benchmark in
291+
for date in testDates {
292+
let dc = gmtCalendr.dateComponents([.month, .day], from: date)
293+
blackHole(dc)
294+
}
295+
}
296+
297+
Benchmark("GregorianCalendar-dateComponents-year", configuration: .init(scalingFactor: .mega)) { benchmark in
298+
for date in testDates {
299+
let dc = gmtCalendr.dateComponents([.year], from: date)
300+
blackHole(dc)
301+
}
302+
}
303+
304+
Benchmark("GregorianCalendar-dateComponents-week-based", configuration: .init(scalingFactor: .mega)) { benchmark in
305+
for date in testDates {
306+
let dc = gmtCalendr.dateComponents([.yearForWeekOfYear, .weekOfYear], from: date)
307+
blackHole(dc)
308+
}
309+
}
254310
}
255311

Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift

Lines changed: 141 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1946,12 +1946,14 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
19461946
}
19471947

19481948
func dayOfYear(fromYear year: Int, month: Int, day: Int) throws (GregorianCalendarError) -> Int {
1949+
precondition(month > 0 && month < 13)
19491950
let daysBeforeMonthNonLeap = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
19501951
let daysBeforeMonthLeap = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335]
19511952

19521953
let julianDay = try Self.julianDay(ofDay: day, month: month, year: year)
19531954
let useJulianCalendar = julianDay < julianCutoverDay
19541955
let isLeapYear = gregorianYearIsLeap(year)
1956+
19551957
var dayOfYear = (isLeapYear ? daysBeforeMonthLeap : daysBeforeMonthNonLeap)[month - 1] + day
19561958
if !useJulianCalendar && year == gregorianStartYear {
19571959
// Use julian's week number for 1582, so recalculate day of year
@@ -1985,96 +1987,145 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
19851987
}
19861988

19871989
func dateComponents(_ components: Calendar.ComponentSet, from d: Date, in timeZone: TimeZone) -> DateComponents {
1990+
guard !components.isEmpty else {
1991+
return DateComponents()
1992+
}
1993+
19881994
let timezoneOffset = timeZone.secondsFromGMT(for: d)
19891995
let localDate = d + Double(timezoneOffset)
19901996

19911997
let dateOffsetInSeconds = localDate.timeIntervalSinceReferenceDate.rounded(.down)
19921998
let date = Date(timeIntervalSinceReferenceDate: dateOffsetInSeconds) // Round down the given date to seconds
19931999

1994-
let totalSeconds = Int(dateOffsetInSeconds)
1995-
let secondsInDay = (totalSeconds % 86400 + 86400) % 86400
1996-
1997-
let hour = secondsInDay / 3600
1998-
let minute = (secondsInDay % 3600) / 60
1999-
let second = secondsInDay % 60
2000-
let nanosecond = Int((localDate.timeIntervalSinceReferenceDate - dateOffsetInSeconds) * 1_000_000_000)
2000+
let hour: Int?
2001+
let second: Int?
2002+
let minute: Int?
2003+
let nanosecond: Int?
2004+
var dayOfYear: Int?
2005+
var weekday: Int?
2006+
var weekOfMonth: Int?
2007+
var yearForWeekOfYear: Int?
2008+
var weekdayOrdinal: Int?
2009+
var weekOfYear: Int?
2010+
var year: Int?
2011+
var month: Int?
2012+
var day: Int?
2013+
2014+
let timeComponents: Calendar.ComponentSet = [.hour, .minute, .second, .nanosecond]
2015+
if !components.isDisjoint(with: timeComponents) {
2016+
let totalSeconds = Int(dateOffsetInSeconds)
2017+
let secondsInDay = (totalSeconds % 86400 + 86400) % 86400
2018+
2019+
hour = secondsInDay / 3600
2020+
minute = (secondsInDay % 3600) / 60
2021+
second = secondsInDay % 60
2022+
nanosecond = Int((localDate.timeIntervalSinceReferenceDate - dateOffsetInSeconds) * 1_000_000_000)
2023+
} else {
2024+
hour = nil
2025+
minute = nil
2026+
second = nil
2027+
nanosecond = nil
2028+
}
20012029

2002-
if components.containsOnlyTimeComponents {
2030+
if components.isSubset(of: timeComponents) {
20032031
var dcHour: Int?
20042032
var dcMinute: Int?
20052033
var dcSecond: Int?
20062034
var dcNano: Int?
2035+
20072036
if components.contains(.hour) { dcHour = hour }
20082037
if components.contains(.minute) { dcMinute = minute }
20092038
if components.contains(.second) { dcSecond = second }
20102039
if components.contains(.nanosecond) { dcNano = nanosecond }
2011-
return DateComponents(hour: dcHour, minute: dcMinute, second: dcSecond, nanosecond: dcNano)
2012-
}
2013-
2014-
let dayOfYear: Int
2015-
let weekday: Int
2016-
let weekOfMonth: Int
2017-
var yearForWeekOfYear: Int
2018-
var weekdayOrdinal: Int
2019-
var weekOfYear: Int
2020-
var isLeapYear: Bool
2021-
var year: Int
2022-
var month: Int
2023-
var day: Int
2040+
return DateComponents(rawHour: dcHour, rawMinute: dcMinute, rawSecond: dcSecond, rawNanosecond: dcNano)
2041+
}
2042+
20242043
do {
20252044
let useJulianRef = useJulianReference(date)
20262045
let julianDay = try date.julianDay()
2027-
(year, month, day) = Self.yearMonthDayFromJulianDay(julianDay, useJulianRef: useJulianRef)
2028-
isLeapYear = gregorianYearIsLeap(year)
2029-
2030-
// To calculate day of year, work backwards with month/day
2031-
dayOfYear = try self.dayOfYear(fromYear: year, month: month, day: day)
2032-
func remainder(numerator: Int, denominator: Int ) -> Int {
2033-
let r = numerator % denominator
2034-
return r >= 0 ? r : r + denominator
2035-
}
2036-
// Week of year calculation, from ICU calendar.cpp :: computeWeekFields
2037-
// 1-based: 1...7
2038-
weekday = remainder(numerator: julianDay + 1, denominator: 7) + 1
2039-
2040-
// 0-based 0...6
2041-
let relativeWeekday = (weekday + 7 - firstWeekday) % 7
2042-
let relativeWeekdayForJan1 = (weekday - dayOfYear + 7001 - firstWeekday) % 7
2043-
weekOfYear = (dayOfYear - 1 + relativeWeekdayForJan1) / 7 // 0...53
2044-
if (7 - relativeWeekdayForJan1) >= minimumDaysInFirstWeek {
2045-
weekOfYear += 1
2046-
}
2047-
2048-
yearForWeekOfYear = year
2049-
// Adjust for weeks at end of the year that overlap into previous or next calendar year
2050-
if weekOfYear == 0 {
2051-
let previousDayOfYear = dayOfYear + (gregorianYearIsLeap(year - 1) ? 366 : 365)
2052-
weekOfYear = weekNumber(desiredDay: previousDayOfYear, dayOfPeriod: previousDayOfYear, weekday: weekday)
2053-
yearForWeekOfYear -= 1
2046+
let julianDayYMD = Self.yearMonthDayFromJulianDay(julianDay, useJulianRef: useJulianRef)
2047+
2048+
year = julianDayYMD.year
2049+
month = julianDayYMD.month
2050+
day = julianDayYMD.day
2051+
2052+
guard let year, let month, let day else {
2053+
preconditionFailure()
2054+
}
2055+
2056+
if !components.isDisjoint(with: [.dayOfYear, .quarter]) || !components.isDisjoint(with: [.weekOfYear, .yearForWeekOfYear]) {
2057+
dayOfYear = try self.dayOfYear(fromYear: year, month: month, day: day)
20542058
} else {
2055-
let lastDayOfYear = (gregorianYearIsLeap(year) ? 366 : 365)
2056-
// Fast check: For it to be week 1 of the next year, the DOY
2057-
// must be on or after L-5, where L is yearLength(), then it
2058-
// cannot possibly be week 1 of the next year:
2059-
// L-5 L
2060-
// doy: 359 360 361 362 363 364 365 001
2061-
// dow: 1 2 3 4 5 6 7
2062-
if dayOfYear >= lastDayOfYear - 5 {
2063-
var lastRelativeDayOfWeek = (relativeWeekday + lastDayOfYear - dayOfYear) % 7
2064-
if lastRelativeDayOfWeek < 0 {
2065-
lastRelativeDayOfWeek += 7
2059+
dayOfYear = nil
2060+
}
2061+
2062+
// Calculate weekday-related fields
2063+
if !components.isDisjoint(with: [.weekday, .weekdayOrdinal, .weekOfMonth, .weekOfYear, .yearForWeekOfYear]) {
2064+
func remainder(numerator: Int, denominator: Int ) -> Int {
2065+
let r = numerator % denominator
2066+
return r >= 0 ? r : r + denominator
2067+
}
2068+
// Week of year calculation, from ICU calendar.cpp :: computeWeekFields
2069+
// 1-based: 1...7
2070+
weekday = remainder(numerator: julianDay + 1, denominator: 7) + 1
2071+
2072+
if !components.isDisjoint(with: [.weekOfYear, .yearForWeekOfYear]) {
2073+
guard let dayOfYear, let weekday else {
2074+
preconditionFailure()
2075+
}
2076+
2077+
// 0-based 0...6
2078+
let relativeWeekday = (weekday + 7 - firstWeekday) % 7
2079+
let relativeWeekdayForJan1 = (weekday - dayOfYear + 7001 - firstWeekday) % 7
2080+
var calculatedWeekOfYear = (dayOfYear - 1 + relativeWeekdayForJan1) / 7 // 0...53
2081+
if (7 - relativeWeekdayForJan1) >= minimumDaysInFirstWeek {
2082+
calculatedWeekOfYear += 1
2083+
}
2084+
2085+
var calculatedYearForWeekOfYear = year
2086+
// Adjust for weeks at end of the year that overlap into previous or next calendar year
2087+
if calculatedWeekOfYear == 0 {
2088+
let previousDayOfYear = dayOfYear + (gregorianYearIsLeap(year - 1) ? 366 : 365)
2089+
calculatedWeekOfYear = weekNumber(desiredDay: previousDayOfYear, dayOfPeriod: previousDayOfYear, weekday: weekday)
2090+
calculatedYearForWeekOfYear -= 1
2091+
} else {
2092+
let lastDayOfYear = (gregorianYearIsLeap(year) ? 366 : 365)
2093+
// Fast check: For it to be week 1 of the next year, the DOY
2094+
// must be on or after L-5, where L is yearLength(), then it
2095+
// cannot possibly be week 1 of the next year:
2096+
// L-5 L
2097+
// doy: 359 360 361 362 363 364 365 001
2098+
// dow: 1 2 3 4 5 6 7
2099+
if dayOfYear >= lastDayOfYear - 5 {
2100+
var lastRelativeDayOfWeek = (relativeWeekday + lastDayOfYear - dayOfYear) % 7
2101+
if lastRelativeDayOfWeek < 0 {
2102+
lastRelativeDayOfWeek += 7
2103+
}
2104+
2105+
if ((6 - lastRelativeDayOfWeek) >= minimumDaysInFirstWeek) && ((dayOfYear + 7 - relativeWeekday) > lastDayOfYear) {
2106+
calculatedWeekOfYear = 1
2107+
calculatedYearForWeekOfYear += 1
2108+
}
2109+
}
20662110
}
2111+
yearForWeekOfYear = calculatedYearForWeekOfYear
2112+
weekOfYear = calculatedWeekOfYear
2113+
}
20672114

2068-
if ((6 - lastRelativeDayOfWeek) >= minimumDaysInFirstWeek) && ((dayOfYear + 7 - relativeWeekday) > lastDayOfYear) {
2069-
weekOfYear = 1
2070-
yearForWeekOfYear += 1
2115+
if components.contains(.weekOfMonth) {
2116+
guard let weekday else {
2117+
preconditionFailure()
20712118
}
2119+
weekOfMonth = weekNumber(desiredDay: day, dayOfPeriod: day, weekday: weekday)
2120+
}
2121+
2122+
if components.contains(.weekdayOrdinal) {
2123+
weekdayOrdinal = (day - 1) / 7 + 1
20722124
}
20732125
}
20742126

2075-
weekOfMonth = weekNumber(desiredDay: day, dayOfPeriod: day, weekday: weekday)
2076-
weekdayOrdinal = (day - 1) / 7 + 1
20772127
} catch {
2128+
// Set error values when calculations fail
20782129
year = .max
20792130
month = .max
20802131
day = .max
@@ -2084,9 +2135,9 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
20842135
yearForWeekOfYear = .max
20852136
weekdayOrdinal = .max
20862137
weekOfYear = .max
2087-
isLeapYear = false
20882138
}
20892139

2140+
20902141
var dcCalendar: Calendar?
20912142
var dcTimeZone: TimeZone?
20922143
var dcEra: Int?
@@ -2110,17 +2161,21 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
21102161
if components.contains(.calendar) { dcCalendar = Calendar(identifier: identifier) }
21112162
if components.contains(.timeZone) { dcTimeZone = timeZone }
21122163
if components.contains(.era) {
2113-
if year < 1 {
2164+
if let year = year, year < 1 {
21142165
dcEra = 0
21152166
} else {
21162167
dcEra = 1
21172168
}
21182169
}
21192170
if components.contains(.year) {
2120-
if year < 1 {
2121-
year = 1 - year
2171+
if var year = year {
2172+
if year < 1 {
2173+
year = 1 - year
2174+
}
2175+
dcYear = year
2176+
} else {
2177+
dcYear = .max
21222178
}
2123-
dcYear = year
21242179
}
21252180
if components.contains(.month) { dcMonth = month }
21262181
if components.contains(.day) { dcDay = day }
@@ -2131,18 +2186,24 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
21312186
if components.contains(.weekday) { dcWeekday = weekday }
21322187
if components.contains(.weekdayOrdinal) { dcWeekdayOrdinal = weekdayOrdinal }
21332188
if components.contains(.quarter) {
2134-
let quarter = if !isLeapYear {
2135-
if dayOfYear < 90 { 1 }
2136-
else if dayOfYear < 181 { 2 }
2137-
else if dayOfYear < 273 { 3 }
2138-
else if dayOfYear < 366 { 4 }
2139-
else { fatalError() }
2189+
let quarter: Int
2190+
if let dayOfYear = dayOfYear, let year {
2191+
let isLeapYear = gregorianYearIsLeap(year)
2192+
quarter = if !isLeapYear {
2193+
if dayOfYear < 90 { 1 }
2194+
else if dayOfYear < 181 { 2 }
2195+
else if dayOfYear < 273 { 3 }
2196+
else if dayOfYear < 366 { 4 }
2197+
else { preconditionFailure("Invalid day of year") }
2198+
} else {
2199+
if dayOfYear < 91 { 1 }
2200+
else if dayOfYear < 182 { 2 }
2201+
else if dayOfYear < 274 { 3 }
2202+
else if dayOfYear < 367 { 4 }
2203+
else { preconditionFailure("Invalid day of year") }
2204+
}
21402205
} else {
2141-
if dayOfYear < 91 { 1 }
2142-
else if dayOfYear < 182 { 2 }
2143-
else if dayOfYear < 274 { 3 }
2144-
else if dayOfYear < 367 { 4 }
2145-
else { fatalError() }
2206+
quarter = 1 // fallback if calculations weren't performed
21462207
}
21472208

21482209
dcQuarter = quarter

0 commit comments

Comments
 (0)