35

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.

Alk
  • 5,215
  • 8
  • 47
  • 116

11 Answers11

31

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)
    }
}
OhadM
  • 4,687
  • 1
  • 47
  • 57
mukaissi
  • 2,441
  • 1
  • 21
  • 12
  • 5
    In order to take account for DST (daylight time savings) the `delta` should be calculated using this formula: `let delta = TimeInterval(timezone.secondsFromGMT(for: self) - initTimeZone.secondsFromGMT(for: self))` Moreover, since this is an "epoch calculation" I'd rename the method too with something describing better its functionality as `epochConversion(from: to:)` – valeCocoa Dec 18 '19 at 23:20
  • Your delta direction is backwards - you should either swap the arguments in the subtraction, or swap the parameter names. However, even once that's fixed: Try converting `2020-03-08T05:00:00Z` from GMT to EST. The result will be off by one hour (see [my answer](https://stackoverflow.com/a/60569015/642783) for details). – dbplunkett May 13 '20 at 16:58
29

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")
Amloelxer
  • 752
  • 2
  • 8
  • 20
  • 2
    How can we convert the date to a string that `dateFromString(string:)` would accept? – ScottyBlades Sep 11 '20 at 09:19
  • @ScottyBlades This answer is for the case where you have a date string with no time offset, as the asker does. If you're starting with a `Date`, it's more efficient to do [a direct `Date` -> `Date` conversion](https://stackoverflow.com/a/60569015/642783). – dbplunkett Oct 17 '20 at 15:53
15

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:

Case 1:

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

Case 2:

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 Dates 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

A note about other answers

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 Dates 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 Dates 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.

dbplunkett
  • 153
  • 1
  • 3
  • 12
11

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
    }
}
quemeful
  • 9,542
  • 4
  • 60
  • 69
  • 4
    I am sorry but i can not completely follow: If your function's name suggest convertingToLocalTime the targetOffset always should be autoupdatingCurrent. I think targetOffset and LocalOffset must be swapped in your code. – iDoc Feb 08 '18 at 05:25
  • 3
    Pass GMT+10, it'll work... Now try GMT+8, it'll crash sorta like every other GMT timezone...So yes, very efficient – Greg432 Apr 18 '18 at 00:50
  • 1
    just a warning, although I slightly modified this code to receive a TimeZone instead a String, it giving me the wrong time by 1 hour(daylightSaving?), maybe it was me but I leave the warning here – João Serra May 30 '22 at 15:31
10

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)
    }    
}
Nikaaner
  • 1,022
  • 16
  • 19
  • 1
    Does this method also take into account daylight saving time differences? – valeCocoa Dec 17 '19 at 16:15
  • @valeCocoa Yes, because different time zones are used for DST and standard time – Nikaaner Dec 17 '19 at 21:06
  • 4
    then the `delta` should be calculated passing the `date` to `secondsFromGMT()`, otherwise how would be possible for it to know if it does fall under daylight saving time period or not. Moreover the order of the parameters for calculating the `delta` was right in @mukaissi method. Hence that would be: `let delta = TimeInterval( targetTimeZone.secondsFromGMT(for: self) - initTimeZone.secondsFromGMT(for: self) )` – valeCocoa Dec 18 '19 at 11:50
  • probably we use it for different purposes or in different circumstances, it turns out for someone fits this way but for other different one – Nikaaner Dec 18 '19 at 15:09
  • 1
    Test your method by feeding with this data: `let gmtTZ = TimeZone(secondsFromGMT: 0)!` `let romeTZ = TimeZone(identifier: "Europe/Rome")!` `let gregCalGMT: Calendar = {` `var cal = Calendar(identifier: .gregorian)` `cal.timeZone = gmtTZ` `return cal` `}()` `let dc = DateComponents(year: 2019, month: 8, day: 15, hour: 12, minute: 30)` `let date = gregCalGMT.date(from: dc)!` `let inRome = date.convert(from: gmtTZ, to: romeTZ)` `print(date)` `print(inRome)` – valeCocoa Dec 18 '19 at 23:06
  • 2
    @valeCocoa is absolutely right about daylight saving times – mbi Apr 17 '20 at 17:00
8

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.

David Berry
  • 40,941
  • 12
  • 84
  • 95
  • 1
    The issue is that I load posts newer than post x with the date set above `(x.date)` , I need to convert the date set above back to GMT, otherwise the date in the user's timezone gets passed to the server and the whole query makes no sense. – Alk Jul 28 '16 at 16:45
  • 1
    That's kind of my point. The only time timezone matters is to the user. Get it from the user and immediately change it to GMT, so it's always in a consistent internal format everywhere it's used. – David Berry Jul 28 '16 at 16:58
  • 1
    Dealing strictly with GMT/UTC except when displaying it to the user is key. Otherwise you will pooping your diapers. – SmileBot Aug 18 '21 at 17:21
4

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)
}
mbi
  • 581
  • 5
  • 13
4

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)!
    }
}
Leon
  • 3,614
  • 1
  • 33
  • 46
  • 2
    Thanks. Apple seems to prefer users to use Calendars explicitly for date operations, which forces them to choose an appropriate Calendar, and because Dates are "dumb", so it's slightly magical to have them doing Calendar logic. So I was trying to be consistent with that, even though my way is certainly wordier. A reasonable compromise might be to allow the Calendar to be specified as an argument to the Date method. – dbplunkett Aug 23 '22 at 12:22
  • 2
    Agreed yep, in our project we use dates against the `current` calendar so this extension and others on `Date` make things a lot easier for us. Thanks for your comprehensive answer. – Leon Aug 23 '22 at 13:35
  • 1
    I like this solution best... One can try to do all the calculations and offsets with timezones and dates through calculations, but all those smarts are already built into Calendar class, and one is less prone to run make errors and oversights using Apple maintained classes :-) – clearlight Nov 18 '22 at 22:30
0

Details

  • Xcode 11.4.1 (11E503a), Swift 5.2

Solution 1

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)
    }
}

Usage of solution 1

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))

Solution 2

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)
    }
}

Usage of solution 2

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))
Community
  • 1
  • 1
Vasily Bodnarchuk
  • 24,482
  • 9
  • 132
  • 127
-1

I suggest

  • you set the GMT timezone on your dateFormatter to get back directly a NSDate in UTC (having only NSDates in UTC is a good practice)
  • when you need to display it you use another NSDateFormatter with the local time zone set on it (it is by default)
  • when you need to send a date to your server, you use dateFormatter again to generate a string
PierreMB
  • 94
  • 4
-2

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) ?? ""
}}