I am having an issue with a date string received from a REST web service and how that is being represented in Swift in different timezones. I have just been able to reproduce this issue, so I am going to relate this specific example.
The information I am getting from the web service is a date-time string and a timezone identifier. In this case, the system is in the America/New_York or Eastern US timezone. I created a playground where I have the following code
let dateFormatString = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
let easternTimeZone = "America/New_York"
let serverDate = "2017-03-01T00:00:00-05:00"
I have a method that takes the date string and the timezone identifier and creates a Date object as follows
func dateForDateString(dateString: String, timeZone: String) -> Date?
{
let formatter = DateFormatter()
if timeZone != ""
{
formatter.timeZone = TimeZone(identifier: timeZone)
}
formatter.dateFormat = dateFormatString
return formatter.date(from: dateString)
}
When I call this method with what's returned from the web service
let easternDate = dateForDateString(dateString: serverDate, timeZone: easternTimeZone)
print("\(easternDate)")
The print statement outputs Optional(2017-03-01 05:00:00 +0000) which is expected. No problems yet. The problem happens when the device is in a different timezone and for the sake of this example, I am testing with Pacific time. In the playground, the easternTime variable shows as Feb 28, 2017, 9:00 PM. Again, not unexpected.
The date is going to be stored in a Firebase database and I don't care about the time at all. So I end up using the Calendar method startOfDay as follows
let myDate = Calendar.current.startOfDay(for: ed)
as you may expect, this returns the Date Feb 28, 2017, 12:00 AM. Obviously not what I want to store in Firebase. The bottom line is that I need the date to be in the timezone the web service returned.
UPDATE
As mentioned in the comments, I also tried to create a Calendar instance and set it's timeZone property to a TimeZone with the identifier received from the server. The code looks like this
var easternCal = Calendar(identifier: .gregorian)
if let etz = TimeZone(identifier: easternTimeZone)
{
easternCal.timeZone = etz
}
if let ed = easternDate
{
let convertedDate = dateToTimeZone(date: ed, toTimezone: easternTimeZone)
let currentCalDate = Calendar.current.startOfDay(for: ed)
let myDate = easternCal.startOfDay(for: ed)
print("\(myDate)")
}
Last night, I could have sworn this didn't work, but when I try it now in the playground, it appears to be working just fine. currentCalDate is the date I don't want, Feb 28, 2017, 12:00 AM since it is using the user's Calendar that has the Pacific timezone set. myDate is correctly showing as Feb 28, 2017, 9:00 PM, which is printing as 2017-03-01 05:00:00 +0000, what I have wanted all along.
I think this one is solved.
As mentioned in the update to my question, creating a Calendar instance and setting the timeZone property to the one returned from the back-end solved my problem.
Related
I'm using this extension to convert a string containing date to Date() object:
extension String {
func toDate() -> Date?{
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/YYYY"
let date = dateFormatter.date(from: self)
return date
}
}
the result always containing a time in it. I'm curious where is the time coming from, why it is not all zero?
print("11/12/2021".toDate())
result is ->
2020-12-19 21:00:00 +0000
In the time that I run the code, it is showing 21:00:00, so why it is 21? I believe It is not related to my time because I run it at different times.
A Date object indicates an instant in time anywhere on the planet, independent of time zone.
A DateFormatter can convert a String to a Date (or a Date to a String, but ignore that for now). When it converts a String to a Date, it may make assumptions about the time of day if that is not included in the String. I believe it assumes that the time is midnight (00:00:00) in the date formatter's time zone. (And by the way, midnight is the starting point of a day, so midnight is zero hours/minutes/seconds into the day. Essentially midnight IS zeros for time.)
So when you call your String extension to convert "11/12/2021" to a Date, the extension creates a DateFormatter which defaults to the device time zone. It creates a Date assuming Midnight in the local time zone.
When you print that date, it gets displayed in GMT.
It looks like your format string has problems though. You're getting the wrong year and month. I think you must be using the wrong month or day string in your formatter. (I always have to look those up when I use them.)
Edit:
You likely want a format string of "MM-dd-yyyy"
(2-digit month, 2-digit day of month, and 4-digit year.)
Lower-case "m" or "mm" is minutes. Upper-case "Y" is for "week of year" based calendars, which you probably don't want.
Try this code:
func toDate() -> Date?{
let dateFormatter = DateFormatter()
let posixLocale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "MM-dd-yyyy"
dateFormatter.locale = posixLocale
let date = dateFormatter.date(from: self)
return date
}
}
And to use it:
let dateString = "12/11/2021"
let date = dateString.toDate()
print(date)
if let date = date {
let convertedDateString = DateFormatter.localizedString(from: date, dateStyle: .medium, timeStyle: .medium)
print(convertedDateString)
} else {
print("Can't convert \(dateString) to a date")
}
That displays "Dec 11, 2021 at 12:00:00 AM" in my US locale (US Date formatting.) Note that since I use the DateFormatter class method localizedString(from:dateStyle:timeStyle:) I see midnight as the displayed time (The time you get from a DateFormatter when you don't specify a time, but displayed in the local time zone.)
The answer is:
when we are converting a string to a Date Object the important part is the time zone that we are converting it to.
for example, if you convert your string date to a UTC time zone when you want to bring it back you have to set the time zone of the date to UTC.
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone(identifier: "UTC")
so this is the reason why when we are printing the Date() object it is deferred from our string date.
extension String {
func toDate() -> Date?{
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "mm-dd-yyyy"
let date = dateFormatter.date(from: self)
return date
}
}
extension Date {
func toString() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "mm-dd-yyyy"
return dateFormatter.string(from: self)
}
}
let stringDate = "01-12-2021"
let date = "01-12-2021".toDate()
let convertBack = date?.toString()
print("(\(stringDate)) -> (\(date!)) -> (\(convertBack!))")
and the result is:
(01-12-2021) -> (2021-01-11 21:01:00 +0000) -> (01-12-2021)
so at the end when we convert back the Date object it will be the same. because that 2 dateFormatter in the extensions are using the default time zone. and if you want to specify a specific time zone you have to declare it in converting from and to string together.
I use the following code to convert a string into a date:
// Input is "06-10-18, 01:30 pm"
func convertStringToDate(string: String) -> Date {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy, hh:mm a"
return formatter.date(from: string)!
}
This works fine on simulators and my devices, however it crashes on return for a couple of client devices.
I tried seeing what was wrong by making it return a string from a date, and on the client devices it returns this:
"06-10-18, 13:30"
Why is it returning differently on a handful of devices?
The returned date is correct, you can check that using the .timeIntervalSince1970 property. What happens is that when you print the date (which is actually just a TimeInterval, aka Double, a specific point in time, independent of any calendar or time zone), it's printing its description property, with the current device settings.
To print a date using your current locale, use this instance method:
let date = convertStringToDate(string: "06-10-18, 01:30 pm")
print(date.description(with: Locale(identifier: "en_US_POSIX")))
I want to transfer a date string to Date.
let a = DateFormatter()
a.dateFormat = "yyyy-MM-dd HH:mm:ss"
guard let datea = a.date(from: "2018-06-21 00:00:00") else {
fatalError("ERROR: Date conversion failed due to mismatched format.")
}
print("ans", datea)
But it always print "ans 2018-06-20 16:00:00 +0000"
Why it could not print the original string date "2018-06-21 00:00:00"?
What wrong with my code ?
A Date is not a string. A Date is a moment in time. It has no clock. It has no time zones. It has no calendar. It is just an instant in time, independent of location or localization.
As a debugging convenience, a Date can be easily converted to a string in a pre-defined format using its .description (which is what print calls). As with all .description methods, you should never use this string for anything but debugging (or possibly logging). There is no promise about what format this string is in.
If you need some specific string representation, then you should use the DateFormatter:
print("ans", a.string(from: datea))
You need to provide timeZone to get the time according to that provided timeZone so to convert UTC time to local time your code should be look like that.
let a = DateFormatter()
a.dateFormat = "yyyy-MM-dd HH:mm:ss"
a.timeZone = TimeZone(abbreviation: "UTC")
let dt = a.date(from: "2018-06-21 00:00:00")
a.timeZone = TimeZone.current
a.dateFormat = "yyyy-MM-dd HH:mm:ss" //change the dateFormat according to your need
let dateString = a.string(from: dt!)
print("now the dateString is \(dateString)")
//printed result (now the dateString is 2018-06-21 05:30:00 )
As Rob Napier says in his answer, a Date object does not have a time zone. It represents a moment in time anywhere on the planet.
If you want to display a Date as a month, day, year, and time, you need to specify a particular time zone.
If you just print a date, like print(Date()), you get the default description property of the date object, which shows the date expressed in UTC. That's probably not what you want.
I defined an extension to Date that lets me see dates expressed in the user's current locale and time zone:
extension Date {
func localString(dateStyle: DateFormatter.Style = .medium, timeStyle: DateFormatter.Style = .medium) -> String {
return DateFormatter.localizedString(from: self, dateStyle: dateStyle, timeStyle: timeStyle)
}
func timeString(timeStyle: DateFormatter.Style = .medium) -> String {
return localString(dateStyle: .none, timeStyle: timeStyle)
}
}
If you add that extension to your project you can use it like this:
print(print("ans", datea.localString())
And you'll see your Date in the device's current time zone. It's very useful for debugging.
Using DateFormatter produces a result that is off by a day (actually 12 hours). Using the following code consistently produces dates that show as the previous day. I've been getting this in a number of applications for a while but just finally got around to digging into it.
extension Date
{
func display() -> String
{
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMMM dd, yyyy"
print(dateFormatter.locale)
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
let txt = dateFormatter.string(from: self)
print(txt)
return txt
}
}
Other questions that were in this vein suggested changing the locale, thus the added code for that, but I checked the locale and the actual date. If I add 8 hours to the date, I get the correct display result, but adding less than that does nothing. Some dates are being retrieved from the birthday field in the Contacts app, which yields dates that have a time of day 00:00:00 UTC. It would seem that I need to convert the date to local time? The timezone on the device is set to the local timezone (Pacific). That wouldn't seem so bad, but dates retrieved from a date picker aren't in UTC time, they're in local time. I haven't been able to figure out how to tell which timezone the date is in since using the calendar class and trying to extract the .timezone component says that "NSCalendarUnitTimeZone cannot be gotten by this method". Any suggestions on how to create a universal date formatter that works in all cases?
A couple of observations:
If your Date object is in UTC time zone, then set your formatter’s timeZone to TimeZone(secondsFromGMT: 0), too.
If you’re showing the string representation of a Date object in the UI, you do not want to use a locale of en_US_POSIX. You want to show it in the default locate of the device (i.e., don’t change the formatter’s locale at all). You only use en_US_POSIX when dealing with ISO 8601 and RFC 3339 date strings that are used internally or, for example, for exchanging date strings with a web service).
Finally, I would not specify a dateFormat string because not all users expect dates in MMMM dd, yyyy format. For example, UK users expect it in d MMMM yyyy format. When presenting dates in the UI, specify a dateStyle instead. Or, if none of those styles work, go ahead and specify dateFormat, but set it using setLocalizedDateFormatFromTemplate(_:) rather than a fixed string.
Thus, for your purpose, you would do:
extension Date {
var dateString: String {
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter.string(from: self)
}
}
Or, if you're calling this a lot, you may want to reuse the formatter:
extension Date {
private static let formatterForDateString: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}()
var dateString: String {
return Date.formatterForDateString.string(from: self)
}
}
Use the timeZone property, to get the exact date, as shown as below:
formatter.timeZone = TimeZone(secondsFromGMT: 0)
it will solve your purpose!
I'm having trouble parsing a date string in the PKT time zone.
Here is some code to paste in a playground:
import Foundation
let dateText = "Tue, 27 Jun 2017 04:00 AM PKT"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEE, d MMM yyyy hh:mm a zzz"
if let checkDate = dateFormatter.date(from: dateText) {
let date = checkDate
print(date)
} else {
print("Could not parse")
}
If I change the time zone to "CDT" it works. However, I am trying to parse a date in Pakistan Standard Time (PKT) as that is the format I am parsing.
I found this handy tool and it says the format is invalid.
Using this table as a reference in formatting.
Documentation: https://developer.apple.com/documentation/foundation/nsdateformatter?language=objc
I see a references to similar questions How to get Current EST time IOS Swift