Date Formatter failing on a correct date [duplicate] - swift

I found that DateFormatter date(from:) method can't parse a couple of specific dates. Method returns nil for the 1st april of 1981-1984 years. Is it a bug of Foundation? What can we do to perform parsing of such dates?
Xcode 8.0, iOS SDK 10.0. Here is a screenshot of a short playground example:

This problem occurs if daylight saving time starts exactly on
midnight, as it was the case in Moscow in the years 1981–1984 (see for example Clock Changes in Moscow, Russia (Moskva)).
This was also observed in
Why does NSDateFormatter return nil date for these 4 time zones? and
Why NSDateFormatter is returning null for a 19/10/2014 in a Brazilian time zone?
For example, at midnight of April 1st 1984, the clocks were adjusted one hour forward, which means that the date "1984-04-01 00:00"
does not exist in that timezone:
let dFmt = DateFormatter()
dFmt.dateFormat = "yyyy-MM-dd"
dFmt.timeZone = TimeZone(identifier: "Europe/Moscow")
print(dFmt.date(from: "1984-04-01")) // nil
As a solution, you can tell the date formatter to be "lenient":
dFmt.isLenient = true
and then it will return the first valid date on that day:
dFmt.isLenient = true
if let date = dFmt.date(from: "1984-04-01") {
dFmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
print(dFmt.string(from: date))
}
// 1984-04-01 01:00:00
A different solution
was given by rob mayoff, which is to make the date formatter use noon instead of midnight as the
default date. Here is a translation of rob's code from Objective-C to Swift:
let noon = DateComponents(calendar: dFmt.calendar, timeZone: dFmt.timeZone,
year: 2001, month: 1, day: 1, hour: 12, minute: 0, second: 0)
dFmt.defaultDate = noon.date
if let date = dFmt.date(from: "1984-04-01") {
dFmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
print(dFmt.string(from: date))
}
// 1984-04-01 12:00:00

Related

Convert Double (number of days since Dec 30, 1899) to Date in Swift?

I have a double value that represents the number of days since December 30, 1899 (the usual TDateTime value in Delphi). For example: 43854.4410269444
Is it possible to create a valid Date value from this double?
One way is to get a Date representing "1899-12-30", then call addingTimeInterval.
// either parse a date string...
let formatter = DateFormatter()
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd"
let epoch = formatter.date(from: "1899-12-30")!
// or calculate the time directly from 1970
//let epoch = Date(timeIntervalSince1970: -86400 * 25569)
// now we add the quality of the number of days times the number of seconds in a day
let numberOfDays = 43854.4410269444
let result = epoch.addingTimeInterval(86400 * numberOfDays)
Here's my attempt to solve the problem (see below how I calculated the magic number):
extension Date {
init(fromDelphiTDateTime delphiDate: Double) {
//Number of seconds between 12/30/1899 12:00 AM and 1/1/1970 12:00 AM
let tDateTimeUnixTimeOffset = 2209161600.0
//24 * 60 * 60
let numberOfSecondsInDay = 86400.0
let delphiTDateTimeAsUnixTime = delphiDate * numberOfSecondsInDay - tDateTimeUnixTimeOffset
self.init(timeIntervalSince1970: delphiTDateTimeAsUnixTime)
}
}
I assumed that TDateTime doesn't have any timezone information.
Since the Unix time (timeIntervalSince1970) is in UTC, you'll need to convert your TDateTime value to UTC if it's in a different time zone.
Examples:
//Dec 30, 1899 at 12:00 AM
let date1 = Date(fromDelphiTDateTime: 0)
//Jan 24, 2020 at 11:04 AM
let date2 = Date(fromDelphiTDateTime: 43854.4410269444)
And here's how I calculated the magic number 2209161600.0 using Swift:
let zeroTDateTimeComponents = DateComponents(calendar: Calendar.init(identifier: .gregorian), timeZone: TimeZone(secondsFromGMT: 0), year: 1899, month: 12, day: 30, hour: 0, minute: 0, second: 0)
let zeroTDateTime = zeroTDateTimeComponents.date
let tDateTimeUnixTimeOffset = zeroTDateTime?.timeIntervalSince1970 //the value is 2209161600.0
Hope this helps and I'd be grateful if the community validates or further improves my answer. Some unit tests with the existing know pairs of TDateTime and its double value would certainly help.

Swift Date formating changes to next month when using end of month data?

I live by end of month dates such as "2019-02-28 23:59:59"
print when I print out that date it tells me "Mar-2019". No it is still "Feb-2019"
print(dt1.toString(dateFormat: "MMM-YYYY")
I do use SwiftDate. I also get this issue with the DateFormatter().
so instead of clean code I end up having to subtracting a day to get the correct month-year to display.
Why?
You need to set Timezone & date format properly as below,
let string = "2019-02-28 23:59:59"
let df = DateFormatter()
df.timeZone = .current
df.dateFormat = "yyyy-MM-dd HH:mm:ss"
let date = df.date(from: string)
print(date?.description(with: .current)) //Thursday, February 28, 2019 at 11:59:59 PM Gulf Standard Time
df.dateFormat = "MMM-yyyy"
print(df.string(from: date!)) // Feb-2019

Swift Date function do not work as Expected

I am trying to find out the difference between two date in seconds using Swift 4.1. This is the code I use,
func getDurationInSeconds(date1 :Date) -> Int{
guard let durationInSeconds = Calendar.current.dateComponents([.second], from: Date(), to: date1).second else {
return 0
}
return durationInSeconds
}
Function to generate date1 from 2018-10-09T18:19:00Z
func dateFromString(stringDate:String) -> Date? {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
dateFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX") as Locale?
let date = dateFormatter.date(from: stringDate)
return date
}
The Date is always returning back an hour less than my current device time, so the calculation is not working as expected. If my current device time is 16:34 the Date() function returns it back as 15:34.
I have seen that Date() is returning back the time in UTC not based on my timezone.
At the moment if I pass in a Date 09/10/2018 14:25:00 and the current device time is 09/10/2018 14:20:00. I am expecting this function to return a value 300 which is 60 * 5 minute difference between two dates.
But I am getting back a value of 3900 which is because the date function returns the date as
09/10/2018 13:20:00 instead of 14:20
. So the duration will be 1 hour + the 300 second difference.
Including a sample output from Xcode console, my device time when I executed this code was 2018-10-09 17:56:28.565423
(lldb) po date1
▿ 2018-10-09 17:59:00 +0000
- timeIntervalSinceReferenceDate : 560800740.0
(lldb) po durationInSeconds
3731
(lldb) po Date()
▿ 2018-10-09 16:57:04 +0000
- timeIntervalSinceReferenceDate : 560797024.35021996
(lldb)
But I cant find a proper way to find the correct duration between two times based on my current time zone. How can I do it?
The issue is not with the Date() returning wrong time. Date() always returns the current time, which is not really based on your (any other) local timezone.
The problem seems to be with the dateFormatter that you are using to generate the Date object from the date string.
Please try using the following lines of code:
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
// Not necessary as the dateFormatter would take the device's timeZone to be the default.
dateFormatter.timeZone = Calendar.current.timeZone
instead of:
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
The problem with the latter is that, you are specifying 'Z' to be the zero-offset time zone (UTC). So, the difference of 1 hour from UTC in your device is causing this offset.
And, while passing in the date string, please make sure that you skip the 'Z' at the end (For example, it should be like 2018-10-09T18:19:00).
The updated code should work good for you, and return the expected difference in seconds.
Since you are using a string that represents the current time in your time zone, try this instead:
func getDurationInSeconds(date1: Date) -> Int {
return Int(-date1.timeIntervalSinceNow + TimeZone.current.daylightSavingTimeOffset(for: date1))
}
It uses this property and this method.
Or if you'd like to account for the time zone difference from UTC in dateFromString(stringDate:):
func getDurationInSeconds(date1: Date) -> Int {
return Int(-date1.timeIntervalSinceNow)
}
func dateFromString(stringDate:String) -> Date? {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
let date = dateFormatter.date(from: stringDate)! //I am force-unwrapping for brevity
let adjustedDate = date.addingTimeInterval(-TimeZone.current.daylightSavingTimeOffset(for: date))
return adjustedDate
}
Test
I am in a UTC + 1h timezone:
let str = "2018-10-09T19:50:00Z" //"2018-10-09T19:50:00Z"
let date1 = dateFromString(stringDate: str)! //"Oct 9, 2018 at 7:50 PM"
Date() //At the time of testing this it is "Oct 9, 2018 at 7:53 PM"
getDurationInSeconds(date1: date1) //213, which is 3 minutes and 33 seconds

Swift Calendar class returning wrong date for weekday

I have a question regarding the Calendar class in Swift 3:
Basically, I wanted to get the Date of the next Sunday. However, the following code gives me the wrong output:
let cal = Calendar.current //gregorian
let today = Date(timeIntervalSinceNow: 0)
let date_c = DateComponents(calendar: cal, weekday: 1)
let today_weekday = cal.component(Calendar.Component.weekday, from: today)
let next_sunday = cal.nextDate(after: today, matching: date_c, matchingPolicy: Calendar.MatchingPolicy.nextTime)!
print(today_weekday) //prints out: 1 (today is Sunday, that's true)
print(next_sunday) //prints out: 2017-10-28 which is a Saturday
Please note that I tested the code yesterday, that's why I wrote "today is Sunday".
I was expecting the second print output to be the 2017-10-29 (next Sunday) however, it is giving me Saturday. What am I doing wrong?
Dates are always printed as if they are in the GMT timezone.
Suppose your time zone is UTC+1, then next Sunday will be 29-10-2017 00:00:00 in your time zone. However, this Date instance is expressed as 28-10-2017 23:00:00 in GMT.
So nextDate does return the correct date, it's just not formatted in your time zone. To fix this, just use a DateFormatter:
let formatter = DateFormatter()
formatter.dateStyle = .short
print(formatter.string(from: next_sunday))

Saving Date Components in Swift 3

I have troubles saving a time. Let me show you a screenshot, it will make clear what I'm trying to achieve here.
I am saving a day as an Int and from that I create a new Date, that is handled. Only thing what is missing is adding time to that date. How can I save AM or PM in a date?
Also, will that affect users who use 24-hour time in their iPhones? If I save 9 PM will their system know that I really mean 21 hrs for them?
Okay, I forgot about the code, here it is:
This is a piece of code where I am scheduling a notification
let calendar = Calendar.autoupdatingCurrent
guard let lastPeriod = RealmManager.sharedInstance.queryLastPeriod(), let predictionDate = lastPeriod.predictionDate else {
return
}
let day = predictionDate.day - DefaultsManager.getNotificationDays()
let newComponents = DateComponents(calendar: calendar, timeZone: .current, year: predictionDate.year, month: predictionDate.month, day: day, hour: time, minute: 0)
As you can see that hour part of newComponents is tricky. I only need to provide is it 9AM, 12AM or 9PM. How do I do that? (btw. time is just an Int I'm manually inputting for testing)
As far as I can tell, you use 24h-style hours (what else?).
let dc = DateComponents(year: 2016, month: 01, day: 11, hour: 14, minute: 20)
let d = Calendar.current.date(from: dc)
//> d: Date? = 2016-01-11 13:20:00 UTC
This is correct since my current timezone is CET (UTC+1).
You can use DateFormatter to print dates in different formats. For example:
let df = DateFormatter()
df.dateStyle = DateFormatter.Style.short
df.timeStyle = DateFormatter.Style.short
df.string(from: d!)
//> R4: String = "11/01/16 14:20"
Back to my timezone!
DateFormatter also has a method date(from: String) which you can use to parse dates:
df.date(from: "11/01/16 14:20")
//> R5: Date? = 2016-01-11 13:20:00 UTC
You will have to use settings of Calendar and/or DateFormatter to get 12h-style output. On the input site, you can just map xxAM to Int("xx")! and xxPM to 12 + Int("xx")!.