Skip to content

Commit 0966307

Browse files
authored
Use the canonicalized name to create ICU timezone object (#1616)
ICU expects time zone identifiers to be IANA identifiers, such as "America/Los_Angeles". Hence names such as "UTC+9" aren't officially supported. There is a canonicalization function available, but I did not remember to use it when switching to the new ICU `uatimezone` SPI. Fix this by always using the canonicalized version to initiate an ICU timezone object. 164490980
1 parent 6e7c61d commit 0966307

File tree

3 files changed

+51
-1
lines changed

3 files changed

+51
-1
lines changed

Sources/FoundationInternationalization/TimeZone/TimeZone_ICU.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ internal final class _TimeZoneICU: _TimeZoneProtocol, Sendable {
108108
}
109109

110110
var status = U_ZERO_ERROR
111-
let timeZone : UnsafeMutablePointer<UTimeZone?>? = Array(identifier.utf16).withUnsafeBufferPointer {
111+
// Use the already canonicalized `name` instead of `identifier` to initiate ICU time zone
112+
let timeZone : UnsafeMutablePointer<UTimeZone?>? = Array(name.utf16).withUnsafeBufferPointer {
112113
let uatimezone = uatimezone_open($0.baseAddress, Int32($0.count), &status)
113114
guard status.isSuccess else {
114115
return nil

Tests/FoundationInternationalizationTests/CalendarTests.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,18 @@ private struct CalendarTests {
14361436
try test(Date(timeIntervalSinceReferenceDate: 731154876), Date(timeIntervalSinceReferenceDate: 731842476))
14371437
}
14381438

1439+
@Test func testDateComponentsTimeZone() throws {
1440+
var calendar = Calendar(identifier: .gregorian)
1441+
calendar.timeZone = try #require(TimeZone(identifier: "America/Los_Angeles"))
1442+
1443+
var components = DateComponents(year:2021, month: 6, day: 10, hour: 23, minute: 59, second: 59)
1444+
let tz = try #require(TimeZone(identifier: "UTC+9"))
1445+
components.timeZone = tz
1446+
1447+
let date = try #require(calendar.date(from: components))
1448+
#expect(date.timeIntervalSinceReferenceDate == 645029999)
1449+
}
1450+
14391451
#if _pointerBitWidth(_64) // These tests assumes Int is Int64
14401452
@Test func dateFromComponentsOverflow() {
14411453
let calendar = Calendar(identifier: .gregorian)

Tests/FoundationInternationalizationTests/TimeZoneTests.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ private struct TimeZoneTests {
133133
try testAbbreviation("GMT+8:00", 28800, "GMT+0800")
134134
try testAbbreviation("GMT+0800", 28800, "GMT+0800")
135135
try testAbbreviation("UTC", 0, "GMT")
136+
try testAbbreviation("UTC+9", 32400, "GMT+0900")
137+
try testAbbreviation("UTC+9:00", 32400, "GMT+0900")
138+
try testAbbreviation("UTC+0900", 32400, "GMT+0900")
136139
}
137140

138141
@Test func secondsFromGMT_RemoteDates() {
@@ -296,6 +299,40 @@ private struct TimeZoneICUTests {
296299
try test(.init(year: 2023, month: 11, day: 5, hour: 3, minute: 34, second: 52), expectedRawOffset: -28800, expectedDSTOffset: 0)
297300
}
298301

302+
@Test func names_rawAndDaylightSavingTimeOffset() throws {
303+
var gmt_calendar = Calendar(identifier: .gregorian)
304+
gmt_calendar.timeZone = .gmt
305+
306+
func test(_ identifier: String, _ dateComponent: DateComponents, expectedRawOffset: Int, expectedDSTOffset: TimeInterval, sourceLocation: SourceLocation = #_sourceLocation) throws {
307+
let tz = try #require(_TimeZoneICU(identifier: identifier))
308+
let d = try #require(gmt_calendar.date(from: dateComponent)) // date in GMT
309+
let (rawOffset, dstOffset) = tz.rawAndDaylightSavingTimeOffset(for: d)
310+
#expect(rawOffset == expectedRawOffset, sourceLocation: sourceLocation)
311+
#expect(dstOffset == expectedDSTOffset, sourceLocation: sourceLocation)
312+
}
313+
314+
// PST
315+
// Not in DST
316+
try test("PST", .init(year: 2023, month: 3, day: 12, hour: 1, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 0)
317+
// These times do not exist; we treat it as if in the previous time zone, i.e. not in DST
318+
try test("PST", .init(year: 2023, month: 3, day: 12, hour: 2, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 0)
319+
// After DST starts
320+
try test("PST", .init(year: 2023, month: 3, day: 12, hour: 3, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 3600)
321+
// These times happen twice; we treat it as if in the previous time zone, i.e. still in DST
322+
try test("PST", .init(year: 2023, month: 11, day: 5, hour: 1, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 3600)
323+
// Clock should turn right back as this moment, so if we insist on being at this point, then we've moved past the transition point -- hence not DST
324+
try test("PST", .init(year: 2023, month: 11, day: 5, hour: 2, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 0)
325+
// Not in DST
326+
try test("PST", .init(year: 2023, month: 11, day: 5, hour: 2, minute: 34, second: 52), expectedRawOffset: -28800, expectedDSTOffset: 0)
327+
328+
// JST: not in DST
329+
let dc = DateComponents(year: 2023, month: 3, day: 12, hour: 1, minute: 00, second: 00)
330+
try test("JST", dc, expectedRawOffset: 32400, expectedDSTOffset: 0)
331+
try test("UTC+9", dc, expectedRawOffset: 32400, expectedDSTOffset: 0)
332+
try test("UTC+0900", dc, expectedRawOffset: 32400, expectedDSTOffset: 0)
333+
try test("UTC+9:00", dc, expectedRawOffset: 32400, expectedDSTOffset: 0)
334+
try test("GMT+9", dc, expectedRawOffset: 32400, expectedDSTOffset: 0)
335+
}
299336
}
300337
// MARK: - FoundationPreview disabled tests
301338

0 commit comments

Comments
 (0)