Reputation: 5557
I have a date stored on my online server database which is in GMT
. I load the date and convert it to the user's timezone using the following code :
if let messagedate = oneitem["timestamp"] as? String {
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let date = dateFormatter.dateFromString(messagedate)
let source_timezone = NSTimeZone(abbreviation: "GMT")
let local_timezone = NSTimeZone.systemTimeZone()
let source_EDT_offset = source_timezone?.secondsFromGMTForDate(date!)
let destination_EDT_offset = local_timezone.secondsFromGMTForDate(date!)
let time_interval : NSTimeInterval = Double(destination_EDT_offset - source_EDT_offset!)
let final_date = NSDate(timeInterval: time_interval, sinceDate: date!)
curr_item.date = final_date
}
Now I need to convert the date back to GMT
in order to communicate it to the server, however I'm not sure how to convert it back to GMT
.
Upvotes: 37
Views: 70247
Reputation: 39
Find GMT ( Time Difference )
func findGMT() -> (hour: Int, minute: Int, second: Int) {
var formatterOnlyHour: DateFormatter = {
let formatter1 = DateFormatter()
formatter1.dateFormat = "HH"
formatter1.locale = Locale(identifier: Locale.preferredLanguages.first ?? "en")
return formatter1
}()
var hourGMT: Int = (Int(formatterOnlyHour.string(from: Date())) ?? 0) - (Int(Date().preciseGMTTime) ?? 0)
// print("Your GMT: \(GMTvalue)")
var minuteGMT: Int = hourGMT * 60
var secondGMT: Int = hourGMT * 3600
return (hourGMT, minuteGMT, secondGMT)}
Extention 1:
extension Formatter {
// create static date formatters for your date representations
static let preciseLocalTime: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH"
return formatter
}()
static let preciseGMTTime: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "HH"
return formatter
}()}
Extention 2:
extension Date {
// you can create a read-only computed property to return just the nanoseconds from your date time
var nanosecond: Int { return Calendar.current.component(.nanosecond, from: self) }
// the same for your local time
var preciseLocalTime: String {
return Formatter.preciseLocalTime.string(for: self) ?? ""
}
// or GMT time
var preciseGMTTime: String {
return Formatter.preciseGMTTime.string(for: self) ?? ""
}}
Upvotes: -3
Reputation: 9868
runs more effeciently
extension Date {
func convertToLocalTime(fromTimeZone timeZoneAbbreviation: String) -> Date? {
if let timeZone = TimeZone(abbreviation: timeZoneAbbreviation) {
let targetOffset = TimeInterval(timeZone.secondsFromGMT(for: self))
let localOffeset = TimeInterval(TimeZone.autoupdatingCurrent.secondsFromGMT(for: self))
return self.addingTimeInterval(targetOffset - localOffeset)
}
return nil
}
}
Upvotes: 11
Reputation: 3746
The answer from dbplunkett is exactly right that daylight saving time isn't effectively handled by using secondsFromGMT(for: date)
, however their extension example is for Calendar
. The below extension is for date
which achieves the same aim:
extension Date {
func convert(from timeZone: TimeZone, to destinationTimeZone: TimeZone) -> Date {
let calendar = Calendar.current
var components = calendar.dateComponents(in: timeZone, from: self)
components.timeZone = destinationTimeZone
return calendar.date(from: components)!
}
}
Upvotes: 7
Reputation: 141
At time of writing, most answers contain an edge case bug near DST switchover times (see my note about other answers below). If you just want to convert a date string with no time offset to a Date
in a particular time zone, Amloelxer's answer is best, but for the benefit of those with the question of "how to convert a Date
between timezones", there are two cases:
Convert a Date
to another time zone while preserving the day and time from the initial time zone.
E.g. for GMT to EST: 2020-03-08T10:00:00Z
to 2020-03-08T10:00:00-04:00
Convert a Date
to the day and time from another time zone while preserving the initial time zone.
E.g. for EST to GMT: 2020-03-08T06:00:00-04:00
to 2020-03-08T10:00:00-04:00
(because the initial Date
is 10am in GMT)
These two cases are actually the same (the example start and end Date
s are identical), except they are worded differently to swap which time zone is the "initial" and which is the "target". The two solutions below are therefore equivalent if you swap the time zones between them, so you can choose the one that conceptually fits your use case better.
extension Calendar {
// case 1
func dateBySetting(timeZone: TimeZone, of date: Date) -> Date? {
var components = dateComponents(in: self.timeZone, from: date)
components.timeZone = timeZone
return self.date(from: components)
}
// case 2
func dateBySettingTimeFrom(timeZone: TimeZone, of date: Date) -> Date? {
var components = dateComponents(in: timeZone, from: date)
components.timeZone = self.timeZone
return self.date(from: components)
}
}
// example values
let initTz = TimeZone(abbreviation: "GMT")!
let targetTz = TimeZone(abbreviation: "EST")!
let initDate = Calendar.current.date(from: .init(timeZone: initTz, year: 2020, month: 3, day: 8, hour: 4))!
// usage
var calendar = Calendar.current
calendar.timeZone = initTz
let case1TargetDate = calendar.dateBySetting(timeZone: targetTz, of: initDate)!
let case2TargetDate = calendar.dateBySettingTimeFrom(timeZone: targetTz, of: initDate)!
// print results
let formatter = ISO8601DateFormatter()
formatter.timeZone = targetTz // case 1 is concerned with what the `Date` looks like in the target time zone
print(formatter.string(from: case1TargetDate)) // 2020-03-08T04:00:00-04:00
// for case 2, find the initial `Date`'s time in the target time zone
print(formatter.string(from: initDate)) // 2020-03-07T23:00:00-05:00 (the target date should have this same time)
formatter.timeZone = initTz // case 2 is concerned with what the `Date` looks like in the initial time zone
print(formatter.string(from: case2TargetDate)) // 2020-03-07T23:00:00Z
At time of writing, most other answers assume one of the two above cases, but more importantly, they share a bug - they attempt to calculate the time difference between the time zones, where the sign of the difference determines the case:
Case 1:
initialTz.secondsFromGMT(for: initialDate) - targetTz.secondsFromGMT(for: initialDate)
Case 2:
targetTz.secondsFromGMT(for: initialDate) - initialTz.secondsFromGMT(for: initialDate)
secondsFromGMT
takes the Date
for which you want to know the offset, so in both cases the target offset should really be targetTz.secondsFromGMT(for: targetDate)
, which is a catch-22, since we don't know the target date yet. However, in most cases where the Date
s are close, as they are here, targetTz.secondsFromGMT(for: initialDate)
and targetTz.secondsFromGMT(for: targetDate)
are equal - a bug only occurs when they differ, which happens when the time offset changes between the two Date
s in the target time zone, e.g. for DST. Here is a bugged example for each case:
extension Date {
// case 1 (bugged)
func converting(from initTz: TimeZone, to targetTz: TimeZone) -> Date {
return self + Double(initTz.secondsFromGMT(for: self) - targetTz.secondsFromGMT(for: self))
}
// case 2 (bugged)
func convertingTime(from initTz: TimeZone, to targetTz: TimeZone) -> Date {
return self + Double(targetTz.secondsFromGMT(for: self) - initTz.secondsFromGMT(for: self))
}
}
let formatter = ISO8601DateFormatter()
// case 1
do {
// example values
let initTz = TimeZone(abbreviation: "GMT")!
let targetTz = TimeZone(abbreviation: "EST")!
let initDate = Calendar.current.date(from: .init(timeZone: initTz, year: 2020, month: 3, day: 8, hour: 4))!
// usage
let targetDate = initDate.converting(from: initTz, to: targetTz)
// print results
formatter.timeZone = targetTz // case 1 is concerned with what the `Date` looks like in the target time zone
print(formatter.string(from: targetDate)) // 2020-03-08T05:00:00-04:00 (should be 4am)
}
// case 2
do {
// example values
let initTz = TimeZone(abbreviation: "EST")!
let targetTz = TimeZone(abbreviation: "GMT")!
let initDate = Calendar.current.date(from: .init(timeZone: initTz, year: 2020, month: 3, day: 8, hour: 1))!
// usage
let targetDate = initDate.convertingTime(from: initTz, to: targetTz)
// print results
formatter.timeZone = targetTz // for case 2, find the initial `Date`'s time in the target time zone
print(formatter.string(from: initDate)) // 2020-03-08T06:00:00Z (the target date should have this same time)
formatter.timeZone = initTz // case 2 is concerned with what the `Date` looks like in the initial time zone
print(formatter.string(from: targetDate)) // 2020-03-08T07:00:00-04:00 (should be 6am)
}
If you adjust the example dates just a few hours forwards or backwards, the bug does not occur. Calendrical calculations are complex, and attempting to roll your own will almost always result in buggy edge cases. Since a time zone is a calendrical unit, to avoid bugs, you should use the existing Calendar
interface, as in my initial example.
Upvotes: 18
Reputation: 25294
Based on mukaissi answer
import Foundation
extension Date {
func to(timeZone outputTimeZone: TimeZone, from inputTimeZone: TimeZone) -> Date {
let delta = TimeInterval(outputTimeZone.secondsFromGMT(for: self) - inputTimeZone.secondsFromGMT(for: self))
return addingTimeInterval(delta)
}
}
let utcTimeZone = TimeZone(abbreviation: "UTC")!
let dateString = "2020-06-03T01:43:44.888Z"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
let date = dateFormatter.date(from: dateString)
print(date)
print(date?.to(timeZone: .autoupdatingCurrent, from: utcTimeZone))
print(date?.to(timeZone: .current, from: utcTimeZone))
print(date?.to(timeZone: TimeZone(abbreviation: "PDT")!, from: utcTimeZone))
Do not forget to paste the Solution 1 code here
extension DateFormatter {
func date(from string: String, timeZoneInString: TimeZone, outputTimeZone: TimeZone = .autoupdatingCurrent) -> Date? {
date(from: string)?.to(timeZone: outputTimeZone, from: timeZoneInString)
}
}
let utcTimeZone = TimeZone(abbreviation: "UTC")!
let pdtTimeZone = TimeZone(abbreviation: "PDT")!
let dateString = "2020-06-03T01:43:44.888Z"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
print(dateFormatter.date(from: dateString))
print(dateFormatter.date(from: dateString, timeZoneInString: utcTimeZone))
print(dateFormatter.date(from: dateString, timeZoneInString: utcTimeZone, outputTimeZone: pdtTimeZone))
Upvotes: 0
Reputation: 629
So this is mukaissi's answer enhanced with valeCocoa's suggestion for daylight saving time:
func convert(from initTimeZone: TimeZone, to targetTimeZone: TimeZone) -> Date {
let delta = TimeInterval(targetTimeZone.secondsFromGMT(for: self) - initTimeZone.secondsFromGMT(for: self))
return addingTimeInterval(delta)
}
Upvotes: 4
Reputation: 2471
Simpler version:
extension Date {
func convertToTimeZone(initTimeZone: TimeZone, timeZone: TimeZone) -> Date {
let delta = TimeInterval(timeZone.secondsFromGMT(for: self) - initTimeZone.secondsFromGMT(for: self))
return addingTimeInterval(delta)
}
}
Upvotes: 32
Reputation: 1320
Based on mukaissi's answer, but the order of deductible in the expression has been corrected.
extension Date {
func convert(from initTimeZone: TimeZone, to targetTimeZone: TimeZone) -> Date {
let delta = TimeInterval(initTimeZone.secondsFromGMT() - targetTimeZone.secondsFromGMT())
return addingTimeInterval(delta)
}
}
Upvotes: 12
Reputation: 104
I suggest
dateFormatter
to get back directly a NSDate in UTC (having only NSDates in UTC is a good practice)dateFormatter
again to generate a stringUpvotes: -1
Reputation: 772
Couldn't you just use your data formatter again with a different time zone and convert it? Such as
dateFormatter.timeZone = NSTimeZone(abbreviation: "GMT")
let gmtDate = dateFormatter.dateFromString(string: "your old date as string here")
Upvotes: 29
Reputation: 41236
Since NSDate
is always in GMT/UTC the time zone only becomes relevant when displaying it to, or getting it from, the user. Just always assume it's UTC internally, convert it for the user (by setting it on the NSDateFormatter
) as necessary, and you no longer have to worry about the problem.
Upvotes: 9