How to get NSDate from string without converting it to UTC - ios

I have following problem. Let's suppose that I receive from server some string containing date time in following format:
yyyy-MM-dd'T'HH:mm:ssxxxx
This is a server date time and I would like to display it in application as it is. So if I receive date time:
2017-04-07T10:29:23+ 02:00
it means that on the server is 10:29:23 (08:29:23 UTC)
and I would like to display it in application as:
04.07.2017 10:29:23
but whenever I try to convert the string to NSDate I get the date converted to UTC which is not desired because I lose information about server time zone. I use following code right now:
let formatter = NSDateFormatter()
formatter.calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierISO8601)
formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssxxxx"
let date = formatter.dateFromString(dateString)
How can I achieve that in Swift (2.3)?
Thanks in advance.

NSDate does not have a built-in time zone. If you print an NSDate then it'll print in GMT because it has to print in something. But the date itself does not contain a time zone, and can't. They're time-zone independent.
To print a date in a particular time zone, configure an NSDateFormatter with the format and time zone that you want, then use .stringFromDate. Or don't set a time zone and it'll print in your device's time zone and locale, likely being what your user expects (e.g. your server says "event happened at 00:23+02:00" so to your CET user you'll display "event happened at 23:23", and to your EST user you display "event happened at 6:23PM").
Frustratingly, you have only one problem left: the date formatter understands the time zone in your original string (the +02:00), but can't communicate it to you by any means. So if you want to take an NSDate and print it in the same time zone as your server then you're going to have to determine the time zone on your own, communicate it via another channel, or hard-code it.

Ok, so finally I've managed to resolve the problem using your hints.
Following code meets my expectations:
let formatter = NSDateFormatter()
formatter.calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierISO8601)
formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssxxxx"
let timeZonePattern = "([+-]([01][0-9]):[0-5][0-9])$" // e.g. "+02:00"
let regex = try! NSRegularExpression(pattern: pattern, options: [])
let dateStringWithoutTimeZone = regex.stringByReplacingMatchesInString(dateString, options: [], range: NSMakeRange(0, dateString.characters.count), withTemplate: "Z")
let date = formatter.dateFromString(dateStringWithoutTimeZone)
Thank you all!

Related

DateFormatter returns nil date for some users regardless of locale/isDaylightSavingTime/timezone

We use Disqus for our comments functionality. Its comments timestamp is in ISO8601 date format, e.g. "2019-12-11T01:45:23". We tried to parse that string with DateFormatter, set up like this:
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
return dateFormatter
It works well for most users. However, we receive reports for a small amount of users that the formatter returns nil. Our initial hypothesis for the cause are as follows.
the date format "yyyy-MM-dd'T'HH:mm:ss" was wrong
locale
timezone
daylight saving makes some time theoretically non-existent
calendar
or something else...
All of them are parameters we use to set up the DateFormatter. We tried many combinations, including ones we got from user reports. We tried the same date, locale, timezone, calendar, and the time itself as in user reports.
locale: en_GB, timezone: Europe/London, calendar: gregorian, isDaylightSavingTime: 1
locale: en_TR, timezone: Etc/GMT-3, calendar: gregorian, isDaylightSavingTime: 0
locale: es_MX, timezone: America/Mexico_City, calendar: gregorian, isDaylightSavingTime: 1
locale: en_ID, timezone: Asia/Jakarta, calendar: gregorian, isDaylightSavingTime: 0
But we could not reproduce.
Moreover, we did another test with a bunch of dates spreading throughout the year. And perform hidden parsing test on every user. It seems on device that returns nil date, it returns nil for EVERY date. The date string list looks like this...
[
"2019-01-11T01:45:23",
"2019-02-11T01:45:23",
"2019-03-11T01:45:23",
"2019-04-10T01:45:23",
"2019-04-11T01:45:23",
"2019-04-12T01:45:23",
"2019-05-11T01:45:23",
"2019-06-11T01:45:23",
"2019-07-11T01:45:23",
"2019-08-11T01:45:23",
"2019-09-11T01:45:23",
"2019-10-11T01:45:23",
"2019-11-11T01:45:23",
"2019-12-11T01:45:23",
])
We later found that there is another date formatter called ISO8601DateFormatter. It seems to be more appropriate for this parsing. Here is the how we set it up.
let dateFormatter = ISO8601DateFormatter()
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
dateFormatter.formatOptions = [.withFullDate, .withTime, .withDashSeparatorInDate, .withColonSeparatorInTime]
return dateFormatter
With this ISO8601DateFormatter, the issue is fixed.
But I still want to know what can cause DateFormatter to fail on some device? Is there other factors than locale/isDaylightSavingTime/timezone that I'm not aware of?
Is it the user's setting for 12-hour vs. 24-hour clock?
iOS can change a date formatter's format string to match the user's settings. The locale en_US_POSIX is recommended for fixed formats to prevent format changes due to user settings.
Please use ISO8601 date format with timezone
"2019-12-11T01:45:23.000Z"
Format:
"XXXX-XX-XX" = year, month, day,
"T" = separator
"XX:XX:XX.XXX" = hour, minute, seconds, milliseconds
"Z" = timezone designator for zero offset, a.k.a. UTC, GMT, Zulu time
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
return dateFormatter
For anybody still facing this issue, what worked for me was to set dateFormatter.isLenient = true as it is described here: DateFormatter returning nil for a valid Date string

Preventing Date from being localized

I have the following string:
let dateString = "2018-04-18T04:54:00-04:00"
I initialize a Date via the ISO8601DateForamtter by doing the following:
let formatter = ISO8601DateFormatter()
let date = formatter.date(from: dateString)
If I print the date, I get the following:
Apr 18, 2018 at 1:54am
The formatter is automatically converting the time into my local time. How can I prevent accounting for my time zone? For example, I want the Date object to show the following instead:
Apr 18, 2018 at 4:54am
With ISO8601, 2018-04-18T04:54:00-04:00 means 2018-04-18 04:54:00 in GMT -4h. To print the time as it is in the original string, you need to create a date formatter with the specific time zone which is -4.
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone(secondsFromGMT: -4 * 60 * 60)
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
print(dateFormatter.string(from: date))
You will get
2018-04-17 04:54:00
FYI, I'm adding a link for ISO8601
You need to parse the timezone from your date string and use it to set the timezone from your date formatter:
func secondsFromGMT(from string: String) -> Int {
guard !string.hasSuffix("Z") else { return 0 }
let timeZone = string.suffix(6)
let comps = timeZone.components(separatedBy: ":")
guard let hours = comps.first,
let minutes = comps.last,
let hr = Int(hours),
let min = Int(minutes) else { return 0 }
return hr * 3600 + min * 60
}
let dateString = "2018-04-18T04:54:00-04:00"
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssxxxxx"
formatter.locale = Locale(identifier: "en_US_POSIX")
if let dateFromString = formatter.date(from: dateString) {
formatter.timeZone = TimeZone(secondsFromGMT: secondsFromGMT(from: dateString))
formatter.dateFormat = "MMM dd, yyyy 'at' h:mma"
formatter.amSymbol = "am"
formatter.pmSymbol = "pm"
print(formatter.string(from: dateFromString)) // Apr 18, 2018 at 4:54am
}
Instead of logging the Date directly, have a look at the string(from:timeZone:formatOptions:) method on ISO8601DateFormatter. With this, you should be able to get a date string for any time zone you desire.
You should set your formatter to the appropriate timezone such as (UTC example below):
formatter.timeZone = TimeZone(identifier: "UTC")
or alternatively specify against GMT:
formatter.timeZone = TimeZone(secondsFromGMT: 0)
The date that you are receiving from your current formatter is technically correct. Setting the date backwards as described in the currently accepted answer is not advised because you are effectively hard-coding an intended time zone. As soon as your device enters another time zone (or if a user downloads your app outside of the current time zone), your information will be incorrect.
If you are trying to display this time in the UTC time zone, you need to use another formatter to correctly format the output in the target time zone.
let utcFormatter = DateFormatter()
utcFormatter.timeZone = TimeZone(secondsFromGMT: 0)
// Perform any other transformations you'd like
let output = utcFormatter.string(from: date)
But why is your original date correct?
The Date API is incredibly robust and doing a lot of things under-the-hood, but is effectively implemented using a simple Double. The automaic time-zone information that it's displaying to you is an abstraction to make it easier to reason about. A date technically has no knowledge of what time zone it's in – but converting it to a string implicitly applies an inferred date formatter on the date and returns information it thinks will be most useful to you.
If you're doing manipulations on a date, you're likely using the Calendar API. You typically get a new instance from using Calendar.current, which will create a new calendar with your current time zone information. You can change the represented time zone of the calendar like this:
var calendar = Calendar.current
calendar.timeZone = TimeZone(secondsFromGMT: 0)
This will give you relative dates that will work in any time zone without modifying the base Date object that you're working with.

Convert UTC NSDate into Local NSDate not working

I have a string getting from server as "yyyy-MM-dd'T'HH:mm:ss.SSS'Z"
I convert this string into NSDate by this formate.
class func convertUTCDateToLocateDate(dateStr:String) -> NSDate{
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
dateFormatter.timeZone = NSTimeZone(name: "UTC")
let date = dateFormatter.dateFromString(dateStr)
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z"
dateFormatter.timeZone = NSTimeZone.localTimeZone()
let timeStamp = dateFormatter.stringFromDate(date!)
let dateForm = NSDateFormatter()
dateForm.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
dateForm.timeZone = NSTimeZone.localTimeZone()
let dateObj = dateForm.dateFromString(timeStamp)
return dateObj!
}
Suppose the parameter string is "2016-11-05T12:00:00.000Z" but when i convert this string and return a NSDate object it doesn't change the time according to my local time. I get my correct time in the timeStamp string (in above code). But when i try to convert that timeStamp string into NSDate it again shows that date and time which i got as a parameter.
You shouldnt change a NSDate's time. NSDates are just a point in time, counted by seconds. They have no clue about timezones, days, month, years, hours, minutes, seconds,… If printed directly they will always output the time in UTC.
If you change the date to show you the time of your timezone you are actually altering the time in UTC — hence your date becomes representing another point in time, no matter of the timezone.
Keep them intact by not altering them, instead when you need to display them do it via a date formatter.
If you need to do time calculations that are independent of timezones you also can work with NSDateComponents instead.
NSDate doesn't have a timezone. It's a point in time, independent of anything, especially timezones. You cannot "convert a UTC NSDate to a local NSDate", the statement itself doesn't make any sense.

day for Date() and Calendar.dateComponents don't match up

I'm trying to get the current date to print in a particular format (YYYYMMD) for AWS security credentials and I noticed that when I do Date(), the day is the 4th which is the correct value:
let date = Date()
print("\(date)") //2016-10-04 00:56:28 +0000
Now, I want to print the date in the format I desire so, but I keep getting the day value as the 3rd:
let calendar = Calendar.current
let date = Date()
let components = calendar.dateComponents([.day], from: date)
print("\(components.day)") //Optional(3)
S3 is expecting the date to be the 4th. How can I fix this?
It's because of time zone difference. date will return the UTC time and date but calendar will return the date and time based on your device's time zone. If you need the day number in UTC just set the time zone of the calendar object to UTC after you create it:
let calendar = Calendar.current
calendar.timeZone = TimeZone(abbreviation: "UTC")!
Now it will always match the value that is returned by Date()
When you print a date using
print("\(date)")
You get the date and time in UTC, which is probably not what you want.
If you want to display your date in your local time zone, create a date formatter and use that:
let dateFormatter = NSdateFormatter()
let dateFormatter.dateStyle = .medium
let dateFormatter.timeStyle = .medium
let dateString = dateFormatter.StringFromDate(date)
print ("date = \(dateString)")
If you do this a lot, you might want to create an extension on NSDate displayString so you can use that to display your dates without having to write additional code.

NSDateFormatter decreases the day of date

I need to store Date variable in CoreData in iOS
I need to store the Date only without the Time, So I made a formatter that discard the time partition from the NSDate variable.
But I have a strange result:
This is my code:
let dateStr = "2016-02-14 11:27:01"
let df2 = NSDateFormatter()
df2.timeZone = NSTimeZone.defaultTimeZone()
df2.dateFormat = "yyyy-MM-dd HH:mm:ss"
print(dateStr)
if let date = df2.dateFromString(dateStr) {
df2.dateFormat = "yyyy-MM-dd"
print("-> \(df2.dateFromString(df2.stringFromDate(date)))")
}
and this is the output:
2016-02-14 11:27:01
-> Optional(2016-02-13 20:00:00 +0000)
Why does the formatter decrease the day by one ?
I tried many dates with same issue
Your time zone is obviously UTC+4.
To get UTC set the time zone accordingly.
df2.timeZone = NSTimeZone(forSecondsFromGMT: 0)
But although you see a date 4 hours ago the NSDate object is treated correctly depending on your time zone. The print command displays always UTC ignoring the time zone information, because NSDate is just a wrapper for a Double number.

Resources