Skip to content

Commit 0a6059e

Browse files
authored
CalendarGregorian's date(from: components) does not honor the DateComponents time zone (swiftlang#421)
Use the date component's time zone if there is one. Resolves 122918762
1 parent cf1f212 commit 0a6059e

File tree

3 files changed

+78
-1
lines changed

3 files changed

+78
-1
lines changed

Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1475,9 +1475,10 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
14751475
// MARK:
14761476

14771477
func date(from components: DateComponents) -> Date? {
1478+
// If the components specifies a new time zone, perform this calculation using the specified timezone
14781479
// If the date falls into the skipped time frame when transitioning into DST (e.g. 1:00 - 3:00 AM for PDT), we want to treat it as if DST hasn't happened yet. So, use .former for dstRepeatedTimePolicy.
14791480
// If the date falls into the repeated time frame when DST ends (e.g. 1:00 - 2:00 AM for PDT), we want the first instance, i.e. the instance before turning back the clock. So, use .former for dstSkippedTimePolicy.
1480-
date(from: components, inTimeZone: timeZone, dstRepeatedTimePolicy: .former, dstSkippedTimePolicy: .former)
1481+
date(from: components, inTimeZone: components.timeZone ?? timeZone, dstRepeatedTimePolicy: .former, dstSkippedTimePolicy: .former)
14811482
}
14821483

14831484
// Returns the weekday with reference to `firstWeekday`, in the range of 0...6

Tests/FoundationEssentialsTests/GregorianCalendarTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,42 @@ final class GregorianCalendarTests : XCTestCase {
145145
test(.init(weekOfYear: 43, yearForWeekOfYear: 2935), expected: Date(timeIntervalSince1970: 30477945600.0))
146146
}
147147

148+
func testDateFromComponents_componentsTimeZone() {
149+
let gregorianCalendar = _CalendarGregorian(identifier: .gregorian, timeZone: .gmt, locale: nil, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
150+
151+
let dcCalendar = Calendar(identifier: .japanese, locale: Locale(identifier: ""), timeZone: .init(secondsFromGMT: -25200), firstWeekday: 1, minimumDaysInFirstWeek: 1, gregorianStartDate: nil)
152+
let dc = DateComponents(calendar: nil, timeZone: nil, era: 1, year: 2022, month: 7, day: 9, hour: 10, minute: 2, second: 55, nanosecond: 891000032, weekday: 7, weekdayOrdinal: 2, quarter: 0, weekOfMonth: 2, weekOfYear: 28, yearForWeekOfYear: 2022)
153+
154+
var dc_customCalendarAndTimeZone = dc
155+
dc_customCalendarAndTimeZone.calendar = dcCalendar
156+
dc_customCalendarAndTimeZone.timeZone = .init(secondsFromGMT: 28800)
157+
// calendar.timeZone = UTC+0, dc.calendar.timeZone = UTC-7, dc.timeZone = UTC+8
158+
// expect local time in dc.timeZone (UTC+8)
159+
XCTAssertEqual(gregorianCalendar.date(from: dc_customCalendarAndTimeZone)!, Date(timeIntervalSinceReferenceDate: 679024975.891)) // 2022-07-09T02:02:55Z
160+
161+
var dc_customCalendar = dc
162+
dc_customCalendar.calendar = dcCalendar
163+
dc_customCalendar.timeZone = nil
164+
// calendar.timeZone = UTC+0, dc.calendar.timeZone = UTC-7, dc.timeZone = nil
165+
// expect local time in calendar.timeZone (UTC+0)
166+
XCTAssertEqual(gregorianCalendar.date(from: dc_customCalendar)!, Date(timeIntervalSinceReferenceDate: 679053775.891)) // 2022-07-09T10:02:55Z
167+
168+
var dc_customTimeZone = dc_customCalendarAndTimeZone
169+
dc_customTimeZone.calendar = nil
170+
dc_customTimeZone.timeZone = .init(secondsFromGMT: 28800)
171+
// calendar.timeZone = UTC+0, dc.calendar = nil, dc.timeZone = UTC+8
172+
// expect local time in dc.timeZone (UTC+8)
173+
XCTAssertEqual(gregorianCalendar.date(from: dc_customTimeZone)!, Date(timeIntervalSinceReferenceDate: 679024975.891)) // 2022-07-09T02:02:55Z
174+
175+
let dcCalendar_noTimeZone = Calendar(identifier: .japanese, locale: Locale(identifier: ""), timeZone: nil, firstWeekday: 1, minimumDaysInFirstWeek: 1, gregorianStartDate: nil)
176+
var dc_customCalendarNoTimeZone_customTimeZone = dc
177+
dc_customCalendarNoTimeZone_customTimeZone.calendar = dcCalendar_noTimeZone
178+
dc_customCalendarNoTimeZone_customTimeZone.timeZone = .init(secondsFromGMT: 28800)
179+
// calendar.timeZone = UTC+0, dc.calendar.timeZone = nil, dc.timeZone = UTC+8
180+
// expect local time in dc.timeZone (UTC+8)
181+
XCTAssertEqual(gregorianCalendar.date(from: dc_customCalendarNoTimeZone_customTimeZone)!, Date(timeIntervalSinceReferenceDate: 679024975.891)) // 2022-07-09T02:02:55Z
182+
}
183+
148184
// MARK: - DateComponents from date
149185
func testDateComponentsFromDate() {
150186
let calendar = _CalendarGregorian(identifier: .gregorian, timeZone: TimeZone(secondsFromGMT: 0)!, locale: nil, firstWeekday: 1, minimumDaysInFirstWeek: 5, gregorianStartDate: nil)

Tests/FoundationInternationalizationTests/CalendarTests.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,46 @@ final class GregorianCalendarCompatibilityTests: XCTestCase {
10321032
test(.init(year: 2023, month: 11, day: 5, hour: 3, minute: 34, second: 52))
10331033
}
10341034

1035+
func testDateFromComponents_componentsTimeZone() {
1036+
let timeZone = TimeZone.gmt
1037+
let icuCalendar = _CalendarICU(identifier: .gregorian, timeZone: timeZone, locale: nil, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
1038+
let gregorianCalendar = _CalendarGregorian(identifier: .gregorian, timeZone: timeZone, locale: nil, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
1039+
1040+
1041+
func test(_ dateComponents: DateComponents, file: StaticString = #file, line: UInt = #line) {
1042+
let date_new = gregorianCalendar.date(from: dateComponents)!
1043+
let date_old = icuCalendar.date(from: dateComponents)!
1044+
expectEqual(date_new, date_old, "dateComponents: \(dateComponents)")
1045+
print("""
1046+
1047+
XCTAssertEqual(gregorianCalendar.date(from: dateComponents)!, Date(timeIntervalSinceReferenceDate: \(date_old.timeIntervalSinceReferenceDate))) // \(date_old.formatted(.iso8601))
1048+
""")
1049+
}
1050+
1051+
let dcCalendar = Calendar(identifier: .japanese, locale: Locale(identifier: ""), timeZone: .init(secondsFromGMT: -25200), firstWeekday: 1, minimumDaysInFirstWeek: 1, gregorianStartDate: nil)
1052+
let dc = DateComponents(calendar: nil, timeZone: nil, era: 1, year: 2022, month: 7, day: 9, hour: 10, minute: 2, second: 55, nanosecond: 891000032, weekday: 7, weekdayOrdinal: 2, quarter: 0, weekOfMonth: 2, weekOfYear: 28, yearForWeekOfYear: 2022)
1053+
var dc_customCalendarAndTimeZone = dc
1054+
dc_customCalendarAndTimeZone.calendar = dcCalendar
1055+
dc_customCalendarAndTimeZone.timeZone = .init(secondsFromGMT: 28800)
1056+
test(dc_customCalendarAndTimeZone) // calendar.timeZone = .gmt, dc.calendar.timeZone = UTC-7, dc.timeZone = UTC+8
1057+
1058+
var dc_customCalendar = dc
1059+
dc_customCalendar.calendar = dcCalendar
1060+
dc_customCalendar.timeZone = nil
1061+
test(dc_customCalendar) // calendar.timeZone = .gmt, dc.calendar.timeZone = UTC-7, dc.timeZone = nil
1062+
1063+
var dc_customTimeZone = dc_customCalendarAndTimeZone
1064+
dc_customTimeZone.calendar = nil
1065+
dc_customTimeZone.timeZone = .init(secondsFromGMT: 28800)
1066+
test(dc_customTimeZone) // calendar.timeZone = .gmt, dc.calendar = nil, dc.timeZone = UTC+8
1067+
1068+
let dcCalendar_noTimeZone = Calendar(identifier: .japanese, locale: Locale(identifier: ""), timeZone: nil, firstWeekday: 1, minimumDaysInFirstWeek: 1, gregorianStartDate: nil)
1069+
var dc_customCalendarNoTimeZone_customTimeZone = dc
1070+
dc_customCalendarNoTimeZone_customTimeZone.calendar = dcCalendar_noTimeZone
1071+
dc_customCalendarNoTimeZone_customTimeZone.timeZone = .init(secondsFromGMT: 28800)
1072+
test(dc_customCalendarNoTimeZone_customTimeZone) // calendar.timeZone = .gmt, dc.calendar.timeZone = nil, dc.timeZone = UTC+8
1073+
}
1074+
10351075
func testDateComponentsFromDateCompatibility() {
10361076
let componentSet = Calendar.ComponentSet([.era, .year, .month, .day, .hour, .minute, .second, .nanosecond, .weekday, .weekdayOrdinal, .quarter, .weekOfMonth, .weekOfYear, .yearForWeekOfYear, .calendar])
10371077

0 commit comments

Comments
 (0)