Skip to content
Open
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
75 changes: 72 additions & 3 deletions Benchmarks/Benchmarks/Internationalization/BenchmarkTimeZone.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,17 @@ let benchmarks = {
#endif

let testDates = {
var now = Date.now
let seeds: [Date] = [
Date(timeIntervalSince1970: -2827137600), // 1880-05-30T12:00:00
Date(timeIntervalSince1970: 0),
Date.now,
Date(timeIntervalSince1970: 26205249600) // 2800-05-30
]
var dates: [Date] = []
for i in 0...10000 {
dates.append(Date(timeInterval: Double(i * 3600), since: now))
for seed in seeds {
for i in 0...2000 {
dates.append(Date(timeInterval: Double(i * 3600), since: seed))
}
}
return dates
}()
Expand Down Expand Up @@ -71,6 +78,68 @@ func timeZoneBenchmarks() {
blackHole(t)
}
}

guard let gmtPlus8 = TimeZone(identifier: "GMT+8") else {
fatalError("unexpected failure when creating time zone")
}

let locale = Locale(identifier: "jp_JP")
let gmtOffsetTimeZoneConfiguration = Benchmark.Configuration(scalingFactor: .mega)

var gmtOffsetTimeZoneNames = (0...14).map { "GMT+\($0)" }
gmtOffsetTimeZoneNames.append(contentsOf: (0...12).map{ "GMT-\($0)" })

Benchmark("GMTOffsetTimeZone-creation", configuration: gmtOffsetTimeZoneConfiguration) { benchmark in
for name in gmtOffsetTimeZoneNames {
guard let gmtPlus = TimeZone(identifier: name) else {
fatalError("unexpected failure when creating time zone: \(name)")
}
blackHole(gmtPlus)
}
}

Benchmark("GMTOffsetTimeZone-secondsFromGMT", configuration: gmtOffsetTimeZoneConfiguration) { benchmark in
for d in testDates {
let secondsFromGMT = gmtPlus8.secondsFromGMT(for: d)
blackHole(secondsFromGMT)
}
}

Benchmark("GMTOffsetTimeZone-abbreviation", configuration: gmtOffsetTimeZoneConfiguration) { benchmark in
for d in testDates {
let abbreviation = gmtPlus8.abbreviation(for: d)
blackHole(abbreviation)
}
}

Benchmark("GMTOffsetTimeZone-nextDaylightSavingTimeTransition", configuration: gmtOffsetTimeZoneConfiguration) { benchmark in
for d in testDates {
let nextDST = gmtPlus8.nextDaylightSavingTimeTransition(after: d)
blackHole(nextDST)
}
}

Benchmark("GMTOffsetTimeZone-daylightSavingTimeOffsets", configuration: gmtOffsetTimeZoneConfiguration) { benchmark in
for d in testDates {
let dstOffset = gmtPlus8.daylightSavingTimeOffset(for: d)
blackHole(dstOffset)
}
}

Benchmark("GMTOffsetTimeZone-isDaylightSavingTime", configuration: gmtOffsetTimeZoneConfiguration) { benchmark in
for d in testDates {
let isDST = gmtPlus8.isDaylightSavingTime(for: d)
blackHole(isDST)
}
}

Benchmark("GMTOffsetTimeZone-localizedNames", configuration: gmtOffsetTimeZoneConfiguration) { benchmark in
for style in [TimeZone.NameStyle.generic, .standard, .shortGeneric, .shortStandard, .daylightSaving, .shortDaylightSaving] {
let localizedName = gmtPlus8.localizedName(for: style, locale: locale)
blackHole(localizedName)
}
}

}


4 changes: 4 additions & 0 deletions Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ struct TimeZoneCache : Sendable, ~Copyable {
return offsetFixed(0)
} else if let cached = fixedTimeZones[identifier] {
return cached
} else if let innerTZ = _timeZoneGMTClass().init(identifier: identifier) {
// Identifier takes a form of GMT offset such as "GMT+8"
fixedTimeZones[identifier] = innerTZ
return innerTZ
} else {
if let innerTz = _timeZoneICUClass()?.init(identifier: identifier) {
fixedTimeZones[identifier] = innerTz
Expand Down
7 changes: 6 additions & 1 deletion Sources/FoundationEssentials/TimeZone/TimeZone_GMT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ package final class _TimeZoneGMT : _TimeZoneProtocol, @unchecked Sendable {
let name: String

required package init?(identifier: String) {
fatalError("Unexpected init")
guard let offset = TimeZone.tryParseGMTName(identifier), let offsetName = TimeZone.nameForSecondsFromGMT(offset) else {
return nil
}

self.name = offsetName
self.offset = offset
}

required package init?(secondsFromGMT: Int) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,15 @@ private func _timeZoneGMTClass_localized() -> _TimeZoneProtocol.Type {
internal final class _TimeZoneGMTICU : _TimeZoneProtocol, @unchecked Sendable {
let offset: Int
let name: String


// Allow using this class to represent time zone whose names take form of "GMT+<offset>" such as "GMT+8".
init?(identifier: String) {
fatalError("Unexpected init")
guard let offset = TimeZone.tryParseGMTName(identifier), let offsetName = TimeZone.nameForSecondsFromGMT(offset) else {
return nil
}

self.name = offsetName
self.offset = offset
}

init?(secondsFromGMT: Int) {
Expand Down Expand Up @@ -79,29 +85,23 @@ internal final class _TimeZoneGMTICU : _TimeZoneProtocol, @unchecked Sendable {
default: false
}

// TODO: Consider using ICU C++ API instead of a date formatter here
// TODO: Consider implementing this ourselves
let timeZoneIdentifier = Array(name.utf16)
let result: String? = timeZoneIdentifier.withUnsafeBufferPointer {
var status = U_ZERO_ERROR
guard let df = udat_open(UDAT_NONE, UDAT_NONE, locale?.identifier ?? "", $0.baseAddress, Int32($0.count), nil, 0, &status) else {
return nil
let tz = uatimezone_open($0.baseAddress, Int32($0.count), &status)
defer {
// `uatimezone_close` checks for nil input, so it's safe to do it even there's an error.
uatimezone_close(tz)
}

guard status.isSuccess else {
return nil
}

defer { udat_close(df) }

let pattern = "vvvv"
let patternUTF16 = Array(pattern.utf16)
return patternUTF16.withUnsafeBufferPointer {
udat_applyPattern(df, UBool.false, $0.baseAddress, Int32(isShort ? 1 : $0.count))

return _withResizingUCharBuffer { buffer, size, status in
udat_format(df, ucal_getNow(), buffer, size, nil, &status)
}
let result: String? = _withResizingUCharBuffer { buffer, size, status in
uatimezone_getDisplayName(tz, isShort ? UTIMEZONE_SHORT: UTIMEZONE_LONG, locale?.identifier ?? "", buffer, size, &status)
}
return result
}

return result
Expand Down
57 changes: 57 additions & 0 deletions Tests/FoundationInternationalizationTests/TimeZoneTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,63 @@ private struct TimeZoneTests {
try testAbbreviation("UTC+0900", 32400, "GMT+0900")
}

@Test func timeZoneGMTOffset() throws {
func testName(_ name: String, _ expectedOffset: Int, sourceLocation: SourceLocation = #_sourceLocation) throws {
let tz = try #require(TimeZone(identifier: name))
let secondsFromGMT = tz.secondsFromGMT()
#expect(secondsFromGMT == expectedOffset)
#expect(tz.isDaylightSavingTime() == false)
#expect(tz.nextDaylightSavingTimeTransition == nil)
}

try testName("GMT+8", 8*3600)
try testName("GMT+08", 8*3600)
try testName("GMT+0800", 8*3600)
try testName("GMT+08:00", 8*3600)
try testName("GMT+8:00", 8*3600)
try testName("UTC+9", 9*3600)
try testName("UTC+09", 9*3600)
try testName("UTC+0900", 9*3600)
try testName("UTC+09:00", 9*3600)
try testName("UTC+9:00", 9*3600)
}

@Test(arguments: ["en_001", "en_US", "ja_JP"])
func timeZoneGMTOffset_localizedNames(localeIdentifier: String) throws {
let locale = Locale(identifier: localeIdentifier)
func testNames(
_ names: [String],
_ expectedStandardName: String,
_ expectedShortStandardName: String,
_ expectedDaylightSavingName: String,
_ expectedShortDaylightSavingName: String,
_ expectedGenericName: String,
_ expectedShortGenericName: String,
sourceLocation: SourceLocation = #_sourceLocation) throws {
for name in names {
let tz = try #require(TimeZone(identifier: name))
let standardName = tz.localizedName(for: .standard, locale: locale)
let shortStandardName = tz.localizedName(for: .shortStandard, locale: locale)
let daylightSavingName = tz.localizedName(for: .daylightSaving, locale: locale)
let shortDaylightSavingName = tz.localizedName(for: .shortDaylightSaving, locale: locale)
let generic = tz.localizedName(for: .generic, locale: locale)
let shortGeneric = tz.localizedName(for: .shortGeneric, locale: locale)

#expect(expectedStandardName == standardName)
#expect(expectedShortStandardName == shortStandardName)
#expect(expectedDaylightSavingName == daylightSavingName)
#expect(expectedShortDaylightSavingName == shortDaylightSavingName)
#expect(expectedGenericName == generic)
#expect(expectedShortGenericName == shortGeneric)
}
}

try testNames(["GMT+8", "GMT+08", "GMT+0800", "GMT+08:00", "GMT+8:00"],
"GMT+08:00", "GMT+8", "GMT+08:00", "GMT+8", "GMT+08:00", "GMT+8")
try testNames(["UTC+9", "UTC+09", "UTC+0900", "UTC+09:00", "UTC+9:00"],
"GMT+09:00", "GMT+9", "GMT+09:00", "GMT+9", "GMT+09:00", "GMT+9")
}

@Test func secondsFromGMT_RemoteDates() {
let date = Date(timeIntervalSinceReferenceDate: -5001243627) // "1842-07-09T05:39:33+0000"
let europeRome = TimeZone(identifier: "Europe/Rome")!
Expand Down