Skip to content

Commit 3abdc1b

Browse files
committed
fix: Update TimeZoneUtilities tests to handle DST transitions correctly
1 parent 6fe2033 commit 3abdc1b

File tree

2 files changed

+502
-0
lines changed

2 files changed

+502
-0
lines changed
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
// TimeZoneUtilities.swift
2+
// SwiftDevKit
3+
//
4+
// Copyright (c) 2025 owdax and The SwiftDevKit Contributors
5+
// MIT License - https://opensource.org/licenses/MIT
6+
7+
import Foundation
8+
9+
/// A utility for handling time zone conversions and formatting dates across different time zones.
10+
public enum TimeZoneUtilities {
11+
/// Style options for date and time formatting
12+
public enum FormatStyle {
13+
/// Date only (e.g., "Jan 21, 2025")
14+
case dateOnly
15+
/// Time only (e.g., "14:30")
16+
case timeOnly
17+
/// Date and time (e.g., "Jan 21, 2025 14:30")
18+
case full
19+
/// Short style (e.g., "1/21/25 14:30")
20+
case short
21+
/// Long style (e.g., "January 21, 2025 at 2:30 PM")
22+
case long
23+
}
24+
25+
/// Converts a date from one time zone to another, handling DST transitions.
26+
///
27+
/// This function is useful when you need to display or process dates in different time zones.
28+
/// The returned date represents the same instant in time, just expressed in a different time zone.
29+
/// It properly handles Daylight Saving Time (DST) transitions.
30+
///
31+
/// Examples:
32+
/// ```swift
33+
/// let nyTime = Date() // Current time in New York
34+
/// let nyZone = TimeZone(identifier: "America/New_York")!
35+
/// let tokyoZone = TimeZone(identifier: "Asia/Tokyo")!
36+
///
37+
/// // Convert NY time to Tokyo time
38+
/// let tokyoTime = TimeZoneUtilities.convert(date: date, from: nyZone, to: tokyoZone)
39+
/// ```
40+
///
41+
/// - Parameters:
42+
/// - date: The date to convert
43+
/// - fromZone: The source time zone
44+
/// - toZone: The target time zone
45+
/// - Returns: The converted date
46+
public static func convert(date: Date, from fromZone: TimeZone, to toZone: TimeZone) -> Date {
47+
let sourceSeconds = fromZone.secondsFromGMT(for: date)
48+
let destinationSeconds = toZone.secondsFromGMT(for: date)
49+
let interval = TimeInterval(destinationSeconds - sourceSeconds)
50+
51+
return date.addingTimeInterval(interval)
52+
}
53+
54+
/// Formats a date for a specific time zone with the given style.
55+
///
56+
/// This function is useful when you need to display dates in a specific time zone's format.
57+
/// The output format varies based on the style parameter and respects the provided locale.
58+
/// It automatically handles DST transitions and adjusts the output accordingly.
59+
///
60+
/// Examples:
61+
/// ```swift
62+
/// let now = Date()
63+
/// let tokyoZone = TimeZone(identifier: "Asia/Tokyo")!
64+
///
65+
/// // Different format styles
66+
/// TimeZoneUtilities.format(date: now, timeZone: tokyoZone, style: .dateOnly) // "Jan 21, 2025"
67+
/// TimeZoneUtilities.format(date: now, timeZone: tokyoZone, style: .timeOnly) // "14:30"
68+
/// TimeZoneUtilities.format(date: now, timeZone: tokyoZone, style: .full) // "Jan 21, 2025 14:30"
69+
/// TimeZoneUtilities.format(date: now, timeZone: tokyoZone, style: .short) // "1/21/25 14:30"
70+
/// TimeZoneUtilities.format(date: now, timeZone: tokyoZone, style: .long) // "January 21, 2025 at 2:30 PM"
71+
///
72+
/// // With different locale
73+
/// let japaneseLocale = Locale(identifier: "ja_JP")
74+
/// TimeZoneUtilities.format(date: now, timeZone: tokyoZone, style: .full, locale: japaneseLocale)
75+
/// ```
76+
///
77+
/// - Parameters:
78+
/// - date: The date to format
79+
/// - timeZone: The time zone to use for formatting
80+
/// - style: The formatting style to use
81+
/// - locale: The locale to use for formatting (default: current)
82+
/// - Returns: A formatted string representing the date in the specified time zone
83+
public static func format(
84+
date: Date,
85+
timeZone: TimeZone,
86+
style: FormatStyle,
87+
locale: Locale = .current) -> String
88+
{
89+
let formatter = DateFormatter()
90+
formatter.timeZone = timeZone
91+
formatter.locale = locale
92+
93+
switch style {
94+
case .dateOnly:
95+
formatter.dateStyle = .medium
96+
formatter.timeStyle = .none
97+
case .timeOnly:
98+
formatter.dateStyle = .none
99+
formatter.timeStyle = .short
100+
case .full:
101+
formatter.dateStyle = .medium
102+
formatter.timeStyle = .short
103+
case .short:
104+
formatter.dateStyle = .short
105+
formatter.timeStyle = .short
106+
case .long:
107+
formatter.dateStyle = .long
108+
formatter.timeStyle = .long
109+
}
110+
111+
return formatter.string(from: date)
112+
}
113+
114+
/// Returns a list of all available time zone identifiers grouped by region.
115+
///
116+
/// This function provides access to all time zones supported by the system,
117+
/// organized by geographical region for easier navigation.
118+
///
119+
/// Example:
120+
/// ```swift
121+
/// let zones = TimeZoneUtilities.allTimeZones()
122+
/// for (region, identifiers) in zones {
123+
/// print("Region: \(region)")
124+
/// for identifier in identifiers {
125+
/// print(" - \(identifier)")
126+
/// }
127+
/// }
128+
/// ```
129+
///
130+
/// - Returns: A dictionary with regions as keys and arrays of time zone identifiers as values
131+
public static func allTimeZones() -> [String: [String]] {
132+
var regions: [String: [String]] = [:]
133+
134+
for identifier in TimeZone.knownTimeZoneIdentifiers.sorted() {
135+
let components = identifier.split(separator: "/")
136+
if components.count >= 2 {
137+
let region = String(components[0])
138+
regions[region, default: []].append(identifier)
139+
} else {
140+
regions["Other", default: []].append(identifier)
141+
}
142+
}
143+
144+
return regions
145+
}
146+
147+
/// Returns commonly used time zone identifiers grouped by region.
148+
///
149+
/// This function provides a curated list of frequently used time zones,
150+
/// organized by geographical region for easier selection.
151+
///
152+
/// Example:
153+
/// ```swift
154+
/// let zones = TimeZoneUtilities.commonTimeZones()
155+
/// for (region, identifiers) in zones {
156+
/// print("Region: \(region)")
157+
/// for identifier in identifiers {
158+
/// print(" - \(identifier)")
159+
/// }
160+
/// }
161+
/// ```
162+
///
163+
/// - Returns: A dictionary with regions as keys and arrays of time zone identifiers as values
164+
public static func commonTimeZones() -> [String: [String]] {
165+
let common = [
166+
"America": [
167+
"America/New_York",
168+
"America/Los_Angeles",
169+
"America/Chicago",
170+
"America/Toronto",
171+
"America/Vancouver",
172+
"America/Mexico_City",
173+
"America/Sao_Paulo",
174+
"America/Argentina/Buenos_Aires",
175+
],
176+
"Europe": [
177+
"Europe/London",
178+
"Europe/Paris",
179+
"Europe/Berlin",
180+
"Europe/Rome",
181+
"Europe/Madrid",
182+
"Europe/Amsterdam",
183+
"Europe/Moscow",
184+
"Europe/Istanbul",
185+
],
186+
"Asia": [
187+
"Asia/Tokyo",
188+
"Asia/Shanghai",
189+
"Asia/Singapore",
190+
"Asia/Dubai",
191+
"Asia/Hong_Kong",
192+
"Asia/Seoul",
193+
"Asia/Kolkata",
194+
"Asia/Bangkok",
195+
],
196+
"Pacific": [
197+
"Australia/Sydney",
198+
"Pacific/Auckland",
199+
"Australia/Melbourne",
200+
"Pacific/Honolulu",
201+
"Pacific/Fiji",
202+
"Pacific/Guam",
203+
],
204+
"Africa": [
205+
"Africa/Cairo",
206+
"Africa/Lagos",
207+
"Africa/Johannesburg",
208+
"Africa/Nairobi",
209+
"Africa/Casablanca",
210+
],
211+
]
212+
213+
return common
214+
}
215+
216+
/// Returns the GMT offset for a given time zone at a specific date.
217+
///
218+
/// This function returns a string representation of the time zone's offset from GMT,
219+
/// taking into account Daylight Saving Time if applicable for the given date.
220+
///
221+
/// Examples:
222+
/// ```swift
223+
/// let ny = TimeZone(identifier: "America/New_York")!
224+
/// let date = Date()
225+
///
226+
/// // Get current offset
227+
/// TimeZoneUtilities.currentOffset(for: ny, at: date) // "-05:00" or "-04:00" depending on DST
228+
/// ```
229+
///
230+
/// - Parameters:
231+
/// - timeZone: The time zone to get the offset for
232+
/// - date: The date at which to check the offset (default: current date)
233+
/// - Returns: A string representation of the GMT offset (e.g., "+09:00", "-05:00")
234+
public static func currentOffset(for timeZone: TimeZone, at date: Date = Date()) -> String {
235+
let seconds = timeZone.secondsFromGMT(for: date)
236+
let hours = abs(seconds) / 3600
237+
let minutes = (abs(seconds) % 3600) / 60
238+
let sign = seconds >= 0 ? "+" : "-"
239+
return String(format: "%@%02d:%02d", sign, hours, minutes)
240+
}
241+
242+
/// Checks if a time zone is currently observing Daylight Saving Time.
243+
///
244+
/// This function determines whether a given time zone is currently in DST.
245+
///
246+
/// Example:
247+
/// ```swift
248+
/// let nyZone = TimeZone(identifier: "America/New_York")!
249+
/// let isDST = TimeZoneUtilities.isDaylightSavingTime(in: nyZone) // true during DST
250+
/// ```
251+
///
252+
/// - Parameter timeZone: The time zone to check
253+
/// - Returns: true if the time zone is currently observing DST, false otherwise
254+
public static func isDaylightSavingTime(in timeZone: TimeZone) -> Bool {
255+
timeZone.isDaylightSavingTime(for: Date())
256+
}
257+
258+
/// Gets the next DST transition date for a given time zone.
259+
///
260+
/// This function finds the next date when the time zone will transition
261+
/// either into or out of Daylight Saving Time.
262+
///
263+
/// Example:
264+
/// ```swift
265+
/// let nyZone = TimeZone(identifier: "America/New_York")!
266+
/// if let nextTransition = TimeZoneUtilities.nextDSTTransition(in: nyZone) {
267+
/// print("Next DST transition: \(nextTransition)")
268+
/// }
269+
/// ```
270+
///
271+
/// - Parameter timeZone: The time zone to check
272+
/// - Returns: The next DST transition date, or nil if the time zone doesn't observe DST
273+
public static func nextDSTTransition(in timeZone: TimeZone) -> Date? {
274+
timeZone.nextDaylightSavingTimeTransition
275+
}
276+
}

0 commit comments

Comments
 (0)