DateFormatter returns nil with specific combination of TimeZone and Locale - ios

In our app there is the issue with creating a date from string but is only reproducible with a very specific combination. Unfortunately, there is no way of getting it from the user that experienced the issue, so I decided to just go for it and try every possible combination:
import Foundation
var dateOnlyDateFormatter: (String, String) -> DateFormatter = { timeZoneS, localeS in
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.timeZone = TimeZone(identifier: timeZoneS)
formatter.locale = Locale(identifier: localeS)
return formatter
}
let date = "2022-05-27"
let time = "06:15"
for timeZone in TimeZone.knownTimeZoneIdentifiers {
for locale in Locale.availableIdentifiers {
let dateFormatter = dateOnlyDateFormatter(timeZone, locale)
let printDate = dateFormatter.date(from: date)
if printDate == nil {
print("TimeZone: \(timeZone), Locale: \(locale)")
}
}
}
The result:
TimeZone: America/Asuncion, Locale: ar_SA
TimeZone: America/Asuncion, Locale: en_SA
I am not too sure what is the best way to handle this issue. Obviously our BE could return date using one specific Locale, like en_US_POSIX, but I have very little control over that, being a part of a much bigger older system. Has anybody experienced an issue like that?

If you read the "Working With Fixed Format Date Representations" section of the DateFormatter docs, you'll find:
For most fixed formats, you should also set the locale property to a POSIX locale ("en_US_POSIX"), and set the timeZone property to UTC.
You should probably just follow the advice here... But here's possibly why SA and the Paraguay timezone produces nil.
Further down that section, there is a link to a technical Q&A where this is explained in more detail. The part that is most related to your problem is:
A user can change their calendar (using System Preferences > Language & Region > Calendar on OS X, or Settings > General > International > Calendar on iOS). In that case NSDateFormatter will treat the numbers in the string you parse as if they were in the user's chosen calendar. For example, if the user selects the Buddhist calendar, parsing the year "2010" will yield an NSDate in 1467, because the year 2010 on the Buddhist calendar was the year 1467 on the (Gregorian) calendar that we use day-to-day.
In the locale SA, the numbers of your date string seem to be interpreted using the Islamic Calendar. Take a look at today's date when formatted with en_SA and America/New_York.
let dateFormatter = dateOnlyDateFormatter("America/New_York", "en_SA")
let printDate = dateFormatter.string(from: .init())
print(printDate)
// 1443-10-26
Also take a look at the non-nil dates that is parsed by en_SA and America/New_York
let dateFormatter = dateOnlyDateFormatter("America/New_York", "en_SA")
let printDate = dateFormatter.date(from: date)
print(printDate)
// 2583-10-05 04:00:00 +0000
Notice that 10-05 is the first Sunday of the year 2583 (See this calendar). If Paraguay still uses the same DST rules as it does now in 2583, then it would mean that there is a DST gap transition at 2583-10-05 00:00:00, starting the DST period. The hour starting from 00:00:00 would be skipped, so 00:00:00 would not exist.
When parsing a date only, DateFormatter would try to set the time components to be 00:00:00 in the timezone of the formatter, but 00:00:00 does not exist, so the parsing fails.
In any case, just set locale to posix and timeZone to UTC when you have set dateFormat.

So if you use 'time' like this, there will be no nil values:
let dateOnlyDateFormatter: (String, String) -> DateFormatter = { timeZoneS, localeS in
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
formatter.timeZone = TimeZone(identifier: timeZoneS)
formatter.locale = Locale(identifier: localeS)
return formatter
}
let date = "2022-05-27 06:15"
//let time = "06:15"
for timeZone in TimeZone.knownTimeZoneIdentifiers {
for locale in Locale.availableIdentifiers {
let dateFormatter = dateOnlyDateFormatter(timeZone, locale)
let printDate = dateFormatter.date(from: date)
if printDate == nil {
print(">>>>>>>> TimeZone: \(timeZone), Locale: \(locale)")
} else {
print("..... \(printDate)")
}
}
}

Related

How to convert yyyy-MM-dd'T'HH:mm:ss.SSS'Z' to MM-dd-yyyy in swift

Hello I need to change the date format
I getting a response from backend like
dob = "1989-03-06T00:00:00Z";
in my case, I write the following code but my app is crashed i think my current date format is wrong.
func DateFromWebtoApp(_ date: String) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
let date = dateFormatter.date(from: date)
dateFormatter.dateFormat = "MM-dd-yyyy"
return dateFormatter.string(from: date!)
}
Please look at the date string. There are no milliseconds (S) and the Z is a format specifier (no single quotes).
Further for an arbitrary date format add always a fixed Locale
func dateFromWebtoApp(_ dateString: String) -> String {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
let date = dateFormatter.date(from: dateString)
dateFormatter.dateFormat = "MM-dd-yyyy"
return dateFormatter.string(from: date!)
}
Try this, Its work on my side.
func DateFromWebtoApp(_ date: String) -> String
{
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
let date = dateFormatter.date(from: date)
dateFormatter.dateFormat = "MM-dd-yyyy"
dateFormatter.timeZone = .current
return dateFormatter.string(from: date!)
}
Since that input date string is in a ISO complaint date format you can use ISO8601DateFormatter.
func dateFromWebtoApp(_ inDate: String) -> String? {
let inFormatter = ISO8601DateFormatter()
if let date = inFormatter.date(from: inDate) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM-dd-yyyy"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
return dateFormatter.string(from: date)
}
return nil
}
Since constructing date formatters is somewhat expensive it might be worthwhile to declare them outside of the function and keep them around if possible
Date formatters are notoriously computationally intensive to create. They’re also computationally expensive to change the dateFormat. So I’d suggest you declare two date formatter properties up front, and instantiate them once and only once.
let isoDateFormatter = ISO8601DateFormatter()
let dobFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}()
The first, isoDateFormatter is for converting strings from your backend into Date objects. By using ISO8601DateFormatter, it gets you out of the business of manually configuring the locale for a DateFormatter.
The second, dobFormatter is for converting the date of birth Date object into a string to be presented in your UI. For this second formatter, please note that:
I don’t use dateFormat, but rather dateStyle (so the result honors the particular device’s date localization preferences, where UK users will see “6 Mar 1989”, but US users will see “Mar 6, 2019”). Use .short rather than .medium if you really want to see dd/MM/yy for UK users and MM/dd/yy for US users. But I personally like the medium format, as it is both reasonably concise but also completely unambiguous. Of, if I have space, I use the long format, which represents date most naturally (e.g. “March 6, 2019” rather than “Mar 6, 2019”). Do whatever your UI demands.
But if this is for presentation to the end user, you really should avoid hard-coded date format strings as users want to see dates presented in a manner consistent with their personal device-wide localization settings, not what one app or the other prefers. And there’s no reason to limit your app to US users, because some day you might want to reach a broader audience.
I set the time zone for this output formatter to be GMT because this string represents a date, not a combined date/time. E.g., 1989-03-06T00:00:00Z, which is midnight on March 6, 1989 GMT, translates to is 4pm on March 5th for us in California. But when you present the birthdate, you want to say March 6th, not March 5th. In short, you want to ignore the time and timezone information. You do this by using GMT/UTC/Zulu for your output date formatter.
Anyway, you’d then use it like so:
let input = "1989-03-06T00:00:00Z"
if let dob = isoDateFormatter.date(from: input) {
let output = dobFormatter.string(from: dob)
}

Date() returns hour over 24

I have to send date from my iPhone through my app to server, but I was surprised that the date had stored as NULL (not always) in server, that's the code used for that:
timeS = (Date().millisecondsSince1970)
// make some assertions on timeS
let date = Date(timeIntervalSince1970: timeS)
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
dateFormatter.locale = NSLocale.current
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let dateString = dateFormatter.string(from: date)
return dateString
It seems like the c# code in the server side tried to convert the string to date and it failed, so it was stored NULL.
So recently I tried to send the date without any modifications in a string to see what happened:
self.backS.sendLocation(msg: "data fitched \(Date())")
and that's what I found in the database:
data fitched 2018-05-08 77:45:44 AM +0000
it must be:
data fitched 2018-05-08 07:45:44 AM +0000 (because that was the time for the server)
I don't know why is that happened!!
I really appreciate anybody's help, thanks in advance.
Try to get your current date and time like this in Swift.
let date = Date()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
let result = formatter.string(from: date)
self.backS.sendLocation(msg: "data fetched \(result)")
Your result will be like this.
"2018-05-12 14:34:51 +0000"
To send the date to server you should rather use Unix timestamp, instead of date in string format.
But, if you insist on a string, you should use DateFormatter. When using DateFormatter you should always set the locale property of your formatter. That ensures the formateer will return consistens results, regardless of your phone's regional settings.
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
let dateString = formatter.string(from: Date())
It's important to set the locale property before you set the dateFormat property.
If you don't set the locale property, the formatter might return nil when working with 24-format on devices with 12-hour format set, and vice versa.
Source: Technical Q&A QA1480

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.

cast "1900-01-01T00:00:00" string value to date

I've watching trough stack overflow to find the answer and I can't find it I want to cast this string value "1900-01-01T00:00:00" to Date format, I was trying with some formats like those:
"yyyy-MM-dd HH:mm:ss"
"EEE, dd MMM yyyy hh:mm:ss +zzzz"
"YYYY-MM-dd HH:mm:ss.A"
"yyyy-MM-dd HH:mm:ss.S"
but anyone of those its working.
and I want the date format like this
"dd-mm-yyyy"
Hope you can help me!
Thanks.
It is a two step process, first converting 1900-01-01T00:00:00 (known as a RFC 3999 or ISO 8601 date, referred to the specifications that define this format) into a Date object, and then converting that Date object back to a string in the form of 01-01-1900:
To convert your string in the form of 1900-01-01T00:00:00 into a Date object, you can use ISO8601DateFormatter:
let formatter = ISO8601DateFormatter()
formatter.formatOptions.remove(.withTimeZone)
let date = formatter.date(from: string)!
That is equivalent to the following DateFormat, in which one has to manually set the locale to en_US_POSIX (because RFC 3999/ISO 8601 dates use a Gregorian calendar, regardless of what the device's default calendar type) and sets the timeZone to GMT/Zulu, because usually RFC 3999/ISO 8601 dates are representing GMT unless specified otherwise:
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
let date = formatter.date(from: string)!
For more information about the importance of timezones and locales in parsing RFC 3999 and ISO 8601 dates, see Apple's Technical Q&A 1480.
Then, to convert that Date object to a string into 01-01-1900 (day, month, and year), you'd use a format string of dd-MM-yyyy (note the uppercase MM for "month", to distinguish it from mm for "minute"):
let formatter2 = DateFormatter()
formatter2.dateFormat = "dd-MM-yyyy"
let string = formatter2.string(from: date)
Two observations regarding the dateFormat string:
If this string is for displaying to the user, you might use use dateStyle rather than dateFormat, e.g.:
formatter2.dateStyle = .short
While this will generate a slightly different format, e.g. dd/MM/yy, the virtue of this approach is that the string will be localized (e.g. UK users will see MM/dd/yyyy, their preferred way of seeing short dates).
It just depends upon the purpose of your dd-MM-yyyy format. If it's for internal purposes, go ahead and use dateFormat. But if it's for showing dates in your UI, use dateStyle instead, and enjoy the localization that DateFormatter does automatically for you. For more information, see "Working With User-Visible Representations of Dates and Times" section of the DateFormatter reference.
Note that in the absence of a timeZone specified for this second formatter, it assumes that while the ISO 8601 date was in GMT, that you want to see the date in your local timezone. For example, (1900-01-01T00:00:00 GMT was Dec 31, 1899 at 4pm in California). If you want to see the date string of the original ISO 8601 object, not corrected for timezones, you'd just set the timeZone of this second formatter to be GMT as well, e.g.
formatter2.timeZone = TimeZone(secondsFromGMT: 0)
As others have pointed out, you want to avoid unnecessarily re-instantiating DateFormatter objects. So you might put these formatters in properties that are instantiated only once, or use an extension:
extension DateFormatter {
static let customInputFormatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions.remove(.withTimeZone)
return formatter
}()
static let customOutputFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
formatter.timeZone = TimeZone(secondsFromGMT: 0) // if you want date in your local timezone, remove this line
return formatter
}()
}
And then:
let input = "1900-01-01T00:00:00"
let date = DateFormatter.customInputFormatter.date(from: input)!
let output = DateFormatter.customOutputFormatter.string(from: date)
print(output)
This is how I do custom date formatters:
extension DateFormatter {
static let inDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
return dateFormatter
}()
static let outDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd-mm-yyyy"
return dateFormatter
}()
}
And then use it like:
if let date = DateFormatter.inDateFormatter.date(from: "1900-01-01T00:00:00") {
let newDateString = DateFormatter.outDateFormatter.string(from: date);
print(newDateString) //prints 01-00-1900
}
This avoids any potential performance issues and is clear at the point of use, while still being concise.
Use this extension I created, where you can pass the format as a parameter.
extension String
{
func toDate( dateFormat format : String) -> Date
{
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format
if let date = dateFormatter.date(from: self)
{
return date
}
print("Invalid arguments ! Returning Current Date . ")
return Date()
}
}
"1900-01-01T00:00:00".toDate(dateFormat: "yyyy-MM-dd'T'HH:mm:ss") //Plyground call test

Getting back a date from a string

im dealing with dates, and i'm having problems getting my date back from a string, i simplified my problem here:
let date = NSDate()
let formatter = NSDateFormatter()
formatter.dateFormat = "dd-MM-YYYY"
formatter.stringFromDate(date)
formatter.dateFromString(formatter.stringFromDate(date))
And the output is:
"25-05-2015" (Which is fine)
"Dec 21, 2014, 12:00 AM" (???)
The problem there is that Y is for weekOfYear. You have to use "dd-MM-yyyy". Btw don't forget to set your date formatter locale to "en_US_POSIX" .
If you're working with fixed-format dates, you
should first set the locale of the date formatter to something
appropriate for your fixed format. In most cases the best locale to
choose is "en_US_POSIX", a locale that's specifically designed to
yield US English results regardless of both user and system
preferences. "en_US_POSIX" is also invariant in time (if the US, at
some point in the future, changes the way it formats dates, "en_US"
will change to reflect the new behaviour, but "en_US_POSIX" will not),
and between machines ("en_US_POSIX" works the same on iOS as it does
on OS X, and as it it does on other platforms).
You should use yyyy for the year, not YYYY (which has a different meaning)
let date = NSDate()
let formatter = NSDateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
let s = formatter.stringFromDate(date) // "25-05-2015"
let d = formatter.dateFromString(s) // "2015-05-24 22:00:00 UTC" (*)
(*) it's 22:00 because I'm in the +0200 timezone, so this result is effectively 2015-05-25 00:00:00 in my timezone
You should change your dateFormat to make it work , YYYY is not correct.
import Foundation
let date = NSDate()
let formatter = NSDateFormatter()
formatter.dateFormat = "dd-MM-YYYY"
var firstDate = formatter.stringFromDate(date)
formatter.dateFormat = "yyyy-MM-dd hh:mm:ss.SSSSxxx"
var secondDate = formatter.dateFromString(formatter.stringFromDate(date))
println("\(firstDate)")
println("\(secondDate)")

Resources