简体   繁体   中英

Converting date between timezones swift

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 .

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

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

about 50 times more effecient

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

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.

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

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

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.

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

Eg 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

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 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, eg 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.

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

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM