DateFormatter date from string returns nil when iPhone Date & Time 24-Hour Time is off [duplicate] - swift

This question already has an answer here:
DateFormatter doesn't return date for "HH:mm:ss"
(1 answer)
Closed 2 years ago.
I am working on an app that initializes dates from strings returned from the backend. The dateString is returned using the following format: "2020-03-05T09:00:00+00:00"
The method I have to do the conversion is:
extension Date {
static func convertDate(_ dateString: String?) -> Date? {
guard let dateString = dateString else { return nil }
let dateFormatter = DateFormatter()
dateFormatter.setLocalizedDateFormatFromTemplate("yyyy-MM-dd'T'HH:mm:ssZZZZZ")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
return dateFormatter.date(from: dateString)
}
}
Everything was working fine until someone reported that if the user switches off "24-Hour Time" in settings the method above returns nil.
What am I doing wrong?
Thank you

You're using a very standardized timestamp format, which allows you to take advantage of the ISO8601DateFormatter.
let dateString = "2020-03-05T09:00:00+00:00"
let df = ISO8601DateFormatter()
df.formatOptions = [.withInternetDateTime]
if let date = df.date(from: dateString) {
print(date) // 2020-03-05 09:00:00 +0000
}
If a machine (like your server) is generating the timestamp then it will (should) always be in zulu time (GMT) so you don't need to do anything beyond this. You could specify a time zone but there isn't a point since the string will always zero it out for you.
df.timeZone = TimeZone(secondsFromGMT: 0)
This string represents an absolute moment in time. If you need a relative moment in time, such as the local time from the source, you'll need to identify that time zone and apply it here, which is also very straighforward.

Related

Where the time coming from when converting date string without any time in to Date() in swift?

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.

How to get daylight saving time with using pod TimeZoneLocate in Swift

I'm converting local timezone to string to display it on screen. For this purpose I use TimeZoneLocate library. Problem: I'm getting date result one hour less than it is because of not implementing daylight saving time.
I'm getting JSON from sunrise-sunset.org, and using this lines: sunrise = "3:22:31 AM"; sunset = "5:23:25 PM".
I thought about using function isDaylightSavingTime() with if statement, but I can't figure out where to add this one hour.
This is function where does the magic happen:
func UTCToLocal(incomingFormat: String, outgoingFormat: String, location: CLLocation?) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = incomingFormat
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
let dt = dateFormatter.date(from: self)
let timeZone = location?.timeZone ?? TimeZone.current
dateFormatter.timeZone = timeZone
dateFormatter.dateFormat = outgoingFormat
return dateFormatter.string(from: dt ?? Date())
}
I use local "location" from CLLocation, TimeZone.current is provided by TimeZoneLocate library.
This is how I use it in code:
func parce(json: Data, location: CLLocation) {
let decoder = JSONDecoder()
if let sunriseData = try? decoder.decode(Results.self, from: json) {
self.sunriseLbl.text = sunriseData.results?.sunrise.UTCToLocal(incomingFormat: "h:mm:ss a",
outgoingFormat: "HH:mm",
location: location)
sunriseLbl is printing the sunrise data from JSON for current location by default and for any place by GooglePlaces. But, in both, I get wrong date.
Also, here is a link to my project on GitHub, if it might help you help me: https://github.com/ArtemBurdak/Sunrise-Sunset.
Thanks in advance
An interesting thing I noted: TimeZone.current is returning the correct time zone, but location?.timeZone is not returning the correct time zone. If there is a way to implement TimeZone.current, i.e. the application will always be using the user's current location, then I would advise using that. If users can enter a custom location, however, then you need to get a workaround for the apparent incorrect time zone returned by location?.timeZone.
My workaround is as follows. Notice that we manually adjust the location of the time zone we want by changing the .secondsFromGMT() property. This is how I adjusted your code, and it was returning the correct time zone for my personal location.
extension String {
func UTCToLocal(incomingFormat: String, outgoingFormat: String, location: CLLocation?) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = incomingFormat
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
let dt = dateFormatter.date(from: self)
var timeZone = location?.timeZone ?? TimeZone.current
if timeZone.isDaylightSavingTime() {
timeZone = TimeZone(secondsFromGMT: timeZone.secondsFromGMT() - 7200)!
}
dateFormatter.timeZone = timeZone
dateFormatter.dateFormat = outgoingFormat
let output = dateFormatter.string(from: dt ?? Date())
return output
}
}
NOTE:
Time zones are very complex and change from place to place and from the current time of year. Just because this workaround works for my current location on this current day, doesn't mean that this workaround always works. However, you can look at the timeZone.isDaylightSavingTime() value returned as well as the current location to create a new time zone via timeZone = TimeZone(secondsFromGMT: timeZone.secondsFromGMT() - x as needed. This is the way you can implement the
"I thought about using function isDaylightSavingTime() with if statement, but I can't figure out where to add this one hour."
idea that you had.
EDIT:
For the record, the time zone I was using was CST, or Chicago time. The date I wrote this code at was April 19, 2019.

Swift 4.1 couldn't not convert String to local Date, always return UTC Date

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 which is off by a day

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!

How do I turn an arbitrary time zone into UTC?

I'm allowing my users to be able to select a random Date and time, using their local time zone. I want to be able to send this date string to the server in UTC Format so it can be read by anyone else around the world using their local time zone. I've read lots online on how to turn UTC to local time but not the other way around. How can I accomplish this?
Edit:
First I use the first function to take the concatenated string with a user selected date and time, turn it into a NSDate, and then I convert this NSDate into a UTC string. Is this the best method of achieving my goal?
public class func localTimeZoneStringToDate(string: String) -> NSDate {
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
dateFormatter.timeZone = NSTimeZone.localTimeZone()
return dateFormatter.dateFromString(string)!
}
public class func UTCStringFromDate(date: NSDate) -> String {
let dateFormatter = NSDateFormatter()
dateFormatter.timeZone = NSTimeZone(abbreviation: "UTC")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
return dateFormatter.stringFromDate(date)
}
If you're using the UIDatePicker, getting the selected date and time in UTC is quite simple. The first line of code below will return the selected time in YYYY-MM-DD hh:mm:ss The second line of code will return the time interval in seconds since January 1, 1970 at 12:00am GMT.
// Returns Selecteed Date //
datePicker.date
// Returns Seconds Since Jan. 1, 1970 //
datePicker.date.timeIntervalSince1970
However, if you are not using the UIDatePicker, you can get the same information that the above code will return by simply using:
//** Variable "pastDate" must be NSDate **//
// Returns Selected Date //
pastDate
// Returns Seconds Since Jan.1, 1970 //
pastDate.timeIntervalSince1970
EDIT:
I think I understand what you're attempting to do now, and the following code should return the user's selected date in UTC form.
func UTCStringFromDate(date: NSDate) -> String {
// Get User's Time //
let calendar = NSCalendar.currentCalendar()
// Get User's TimeZone Difference //
let difference = calendar.timeZone.secondsFromGMT
// Get UTC Time //
let adjustedTime = calendar.dateByAddingUnit(.Second, value: -difference, toDate: date, options: [])!
// Format Date //
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
return dateFormatter.stringFromDate(adjustedTime)
}