1

I keep failing to get a consistent list or count of dates between two datetimes when the date is different in the GMT and the user's timezone and when the difference in datetimes is less than 24 hours (even with separate dates). I've googled and keep trying different approaches, but I cannot figure it out. It's even uglier when the user wants to use multiple time zones, but I think that I can solve that once I understand what I am doing wrong here.

In my application, I get dates from the user in several time zones, store them as strings, like: 2017-12-08 12:30:00 +0900

I then attempt to make an array of dates between the start and end date. However, I have not been able to get the array to include all of the actual dates.

First, I tried to expand an array directly between the first and last days: (This works in my project but not in a playground?? I don't know why.

func getMyDates(_ start:String,_ end: String) -> [Date]? {
   dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
   let fromDate = dateFormatter.date(from: start)
   let toDate = dateFormatter.date(from: end)
   var dates = [Date]()
   dates.append(fromDate!)
   dates.append(toDate!)
   print(dates)
   return dates
}
func getDays() -> [Date]? {
    guard let dates = getMyDates() else {return nil}
    print("getDays got \(dates) from getMyDates()")
    var userdates = dates
    let fromDate = dates[0]
    let toDate = dates[1]
    print("changed it to: \(fromDate ... toDate)")
    return fromDate ... toDate
}

This is working if the user's first and last dates are the same for their timezone and the GMT. For example, if the string entries are: 2017-11-08 12:30:00 +0900 and 2017-11-10 23:30:15 +0900, the output includes the three expected dates.

//getDays got [2017-11-08 03:30:00 +0000, 2017-11-10 14:30:15 +0000] from getMyDates()
//changed it to: [2017-11-08 03:30:00 +0000, 2017-11-09 03:30:00 +0000, 2017-11-10 03:30:00 +0000]

However, it fails when the time of day in the second datetime is earlier than the first, like: 2017-12-08 12:30:00 +0900 and 2017-12-09 09:30:15 +0900. In that case, I only get 1 day out:

//getDays got [2017-12-08 03:30:00 +0000, 2017-12-09 01:00:00 +0000] from getTripDates()
//changed it to: [2017-12-08 03:30:00 +0000]

Then, I tried calculating the number of days between them as suggested in this This question, but I get the same problem, as in the first one counts 2 days and the second counts 0 days. My plan was to take the number of days and use this approach.

I think it is a time zone issue, but I can't figure out how to solve it. I've tried setting and not setting the time zone to .current.

I cannot arbitrarily add a day because that will fail when the first datetime is earlier in the day than the second datetime ( like 2017-12-08 12:30:00 +0900 and 2017-12-09 15:30:15 +0900.

Using @malik's suggestion seems like it will work, but somehow it does not. We get the same behavior. If the second datetime TIME is later in the day it doesn't need an additional day (and somehow the seconds remainder does not work - gives too many days.

//in playground swift4
import UIKit

let start = "2017-12-08 12:30:00 +0900"
let end = "2017-12-10 13:30:00 +0900"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
var fromDate = dateFormatter.date(from: start)
let toDate = dateFormatter.date(from: end)
let cal = Calendar.autoupdatingCurrent
let seconds=cal.dateComponents([.second], from: fromDate!, to: toDate!)
var days = Int(seconds.second ?? 0)/(86400)
//days prints 2
//if (Int(seconds.second ?? 0) % (86400)) > 0 {
//    days += 1
//}
var newDates = [Date]()
newDates.append(fromDate!)
for _ in 1 ... days {
    let daysBetween = cal.date(byAdding: .day, value: 1, to: fromDate!)
    fromDate = daysBetween
    newDates.append(fromDate!)
}
//print(newDates)

outputs desired:

// [2017-12-08 03:30:00 +0000, 2017-12-09 03:30:00 +0000, 2017-12-10 03:30:00 +0000]

However: if I change the end date to:

let end = "2017-12-10 09:30:00 +0900"

the output is wrong:

//[2017-12-08 03:30:00 +0000, 2017-12-09 03:30:00 +0000]

Adding in the additional day for partial days helps in this case (uncommenting out the logic:)

if (Int(seconds.second ?? 0) % (86400)) > 0 {
    days += 1
}

yields correctly:

//[2017-12-08 03:30:00 +0000, 2017-12-09 03:30:00 +0000, 2017-12-10 03:30:00 +0000]

BUT: if I change end back while leaving the logic in, I get:

//[2017-12-08 03:30:00 +0000, 2017-12-09 03:30:00 +0000, 2017-12-10 03:30:00 +0000, 2017-12-11 03:30:00 +0000]

I thought it would be fine, but with the additional day, it messes up my later calls to see what else is going on those days because the extra day isn't in the original data.

So, maybe I need to just set up some comparisons of the HH:mm strings? It just doesn't make sense to me. I feel like I'm missing something obvious that would allow me to expand dates in the users' locale.

Using @malik's revised suggestion which lops off the hours and then calculates days works. Then, I can loop over the number of days to create an array.

jessi
  • 1,438
  • 1
  • 23
  • 36

1 Answers1

1

It has nothing to do with time zone. You were on the right plan with the two links. The only problem was that the first link you provided calculated the number of days which doesn't take into account fine grain detail like hour, minutes seconds. Since you want it fine grained, all you need to do is replace .day with .second

extension Date {
    func secondsBetweenDateTimes(toDateTime: Date) -> Int {
        let components = Calendar.current.dateComponents([.second], from: self, to: toDateTime)
        return components.second ?? 0
    }
}

This will return you total number of seconds between your two date times. Next, you need to divide the result from this method by 86400 (number of seconds in a day) to get the number of days. Then use the remainder, divide it by 24 to get the remaining hours and so on and so forth. This way, you can get the exact difference between two DateTimes.

UPDATE

Your if check is a bit faulty. I see what you are trying to do here but that is not the correct if check. Right now what you are telling your code to do is pretty much add an extra day if the number of seconds cannot be divided into whole days. As a result, you will see the behaviour that you are experiencing right now. In my original answer, I said to use the remainder not the whole value to decide whether you want to add a day or not. Anyways, since you are not using the hours, minutes, seconds as your deciding factor, you can do it another way. Just strip out the hour minutes and seconds before calculation and calculate the number of days as below.

let start = "2017-12-08 12:30:00 +0900"
let end = "2017-12-10 13:30:00 +0900"
let startDate = start.substring(to: start.index(start.startIndex, offsetBy: 10))
let endDate = end.substring(to: end.index(end.startIndex, offsetBy: 10))
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
var fromDate = dateFormatter.date(from: startDate)
let toDate = dateFormatter.date(from: endDate)
let cal = Calendar.autoupdatingCurrent
let days=cal.dateComponents([.day], from: fromDate!, to: toDate!)
print(days.day)    //Prints 2
Malik
  • 3,763
  • 1
  • 22
  • 35
  • You are strongly discouraged from doing date math with the 86400 *constant*. When daylight saving times changes days can have 23 or 25 hours. `Calendar` provides much more reliable methods containing *granularity* parameters. – vadian Oct 06 '17 at 08:30
  • @vadian Care to expand on that? – Malik Oct 06 '17 at 08:32
  • Yes. I'm interested in details @vadian I tried converting each date to midnight of the date, but I also did not end up with the correct amount of days. – jessi Oct 06 '17 at 08:44
  • So @Malik - I tried your solution, and in the case where there really are less than 24 hours, I will get less than 86,400 seconds, but I can say that any remainder is +1. This means that it will work as long as I don't face the case where there is daylight savings. I'm going to implement this for now...rounding up. – jessi Oct 06 '17 at 08:51
  • Yep. That's the idea. I don't think daylight savings is relevant in this case. – Malik Oct 06 '17 at 08:53
  • Wait. It has the same fail. I'll update my question. – jessi Oct 06 '17 at 09:41
  • The revised answer works. I am able to use days.day as the count of days to loop over and create a new array of dates. – jessi Oct 13 '17 at 04:14
  • Glad to be of help – Malik Oct 13 '17 at 04:15