Midnights between NSDates

I'm trying to find a way of calculating the number of midnights between dates. For example, if one NSDate is "2016.07.17 23:00" and another is ""2016.07.18 01:00", I want to get a result of 1. But when I use this code:


let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"

let date1 = formatter.dateFromString("2016-07-17 23:00")
let date2 = formatter.dateFromString("2016-07-18 01:00")

let calendar = NSCalendar.currentCalendar()
let day1 = calendar.ordinalityOfUnit(NSCalendarUnit.Day, inUnit: NSCalendarUnit.Era, forDate: date1!)
let day2 = calendar.ordinalityOfUnit(NSCalendarUnit.Day, inUnit: NSCalendarUnit.Era, forDate: date2!)


day1 and day2 have the same value. I'm in the Pacific time zone, so I suppose the problem is that date1 and date2 have no midnight between them in GMT; for when I set the time of date1 to 16:00, day2 - day1 = 1.


How can I get this to work properly whatever the app user's local time zone?

This seems to do what I want:


func midnightsBetweeDates(firstDate: NSDate, secondDate: NSDate) -> Int {
  let calendar = NSCalendar.currentCalendar()
  let midnight1 = calendar.startOfDayForDate(firstDate)
  let midnight2 = calendar.startOfDayForDate(secondDate)
  let day1 = calendar.ordinalityOfUnit(NSCalendarUnit.Day, inUnit: NSCalendarUnit.Era, forDate: midnight1)
  let day2 = calendar.ordinalityOfUnit(NSCalendarUnit.Day, inUnit: NSCalendarUnit.Era, forDate: midnight2)
  return abs(day1 - day2)
}

Note this gives you the number of DAYS betwen those two days, NOT the number of midnights. It's possible to have two midnights in a single day. So if you really wanted the # of midnights, you have more work to do.

I'm trying to find a way of calculating the number of midnights between dates.

How do you want to handle the case where there’s no midnight between dates? Or two midnights? This crops up in the case of the Brazilian states that do daylights savings time, where they move the clocks forward and backwards at midnight!

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I didn't think of that. But all I really want is the number of days. I'll substitute "sleep" for "midnight" throughout.

I believe the code you posted will fail if the era changes. That’s unlikely to be a problem for the Gregorian calendar, where the last era change was 2000 years ago and there probably won’t be any more, but it is a problem for other calendars. For example, in the Japanese calendar the era changes on the death of the emperor (btw that’s the day of their death, so tomorrow could be in a different era than today!).

I think the droid you’re looking for is

components(_:fromDate:toDate:options:)
but, honestly, I’m still a little fuzzy on your exact requirements to be 100% sure that this is the right approach.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I'm not looking for much. All I want is to be able to count integral days betwee NSDates, in local time. So for example, let X be any NSDate on 20 July (local time). Then the difference between X and any NSDate on 21 July (local time) should be 1, between X and any NSDate on 22 July (local time) should be 2, and so on.


One problem I had with one of the functions I tried was due to NSDate being in GMT. So e.g. I would get a difference of 0 between 21:00 on Wednesday and 01:00 on Thursday, since I'm in the Pacific time zone.


I'm not too worried about era changes. The Japanese emperor is relatively young and in good health, and I'm starting to suspect that Jesus won't be coming back after all.

NSDate has no knowledge of timezones. It's just a point in time. The *display* when you print it automatically shows in your local timezone. But the NSDate object itself, and thus your calculations, don't care about timezone.


If you're working with dates, and you want to pretend like the time component doesn't exist, set it to noon, as Apple has explained in the WWDC videos on date manipulation. They're definitely worth a watch! This helps:


extension NSDate {
    func noon() -> NSDate {
        let calendar = NSCalendar.currentCalendar()
        return calendar.dateBySettingHour(12, minute: 0, second: 0, ofDate: self, options: [])!
    }
}


Then, when you want an NSDate without a time component, just call:


myDate = myDate.noon()

>> I'm not looking for much.


Actually, it doesn't sound like much, but it is really hard to state what comparison you're looking for, exactly. Easy to say vaguely, hard to say precisely.


Anyway, I think Quinn gave you the correct answer (components(_:fromDate:toDate:options:)) to your "vague" question, with the proviso that it might not be the answer to the "precise" question you really wanted to ask.


>> The Japanese emperor is relatively young and in good health


Strangely, I believe I heard on the radio recently that the Japanese emperor is thinking about abdicating. FWIW.

I'm working on a very simple app that will let the user keep track of books, tools, lawnmowers, etc. that they lend to friends and neighbours. When the user records the loan of an item, the time is recorded as NSDate().


I want the day count function to return how many days the item has been borrowed, in such a way that if X is borrowed e.g. on Saturday afternoon, and returned on Sunday morning (i.e. the next day, but less than 24 hours later), the function still returns 1.


Maybe it was a mistake to record the loan date simply as NSDate(). Should I have extracted some date components and stored those instead?


I now realize I should have been more specific about my requirements in my first post. I'll remember to do so in future.


Thanks to all those who've responded.


dkj


P.S. I've located two WWDC videos on date and time calculations. I'm watching them now, with a Swift playground open.

Take the date, run it through my noon() method from above, and save that. Depending on 'where' you save it determines 'how' you save it. For example, if you're storing it in Core Data, you can just save the NSDate directly. If you're passing it over to some type of webserver that stores it, then pass `mydate.timeIntervalSince1970` instead, and then when you get it back from the webserver convert it back to a date via `NSDate(timeIntervalSince1970: ...)`.


Remember, you do NOTHING with timezones. That's only for when you want to display that date.


You can then use the `components(_:fromDate:toDate:options:)` that Eskimo told you about to figure out how many days it was gone.

Accepted Answer

I've done as you and Quinn suggested. Now it all works just as I wanted it to. Many thanks!


For the archives, here's all the code in one place:


extension NSDate {
  func noon() -> NSDate {
  let calendar = NSCalendar.currentCalendar()
  return calendar.dateBySettingHour(12, minute: 0, second: 0, ofDate: self, options: [])!
  }
}


  var howManyDays: Int {
  let firstDate = self.dateLoaned.noon()
  let secondDate = NSDate().noon()
  let components = NSCalendar.currentCalendar().components(.Day, fromDate: firstDate, toDate: secondDate, options: [])
  return components.day
  }

Remember, you do NOTHING with timezones.

That’s certainly a valid point of view. However, IMO it’s not the right approach for the described user interface. Let’s say I’m at home in Cupertino (GMT-8 or GMT-7) and loan a hammer to my neighbour, Maxwell. I then go visit my family in Sydney (GMT+10 or GMT+11) and, while I’m there, run the app. Two points:

  • What should the app show as the date that the tool was loaned? IMO it should show that date in Pacific time because that’s how I think about the event. That is, I loaned Maxwell my hammer last Wednesday.

  • OTOH, the number of days that have passed since the event should be time zone independent. You don’t want the difference in timezones artificially increasing or decreasing the day count. In this case I’m not sure if you should be calculating the days in the original time zone, the current time zone, or whether that even matters.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Oh, btw, this discussion is very reminiscent of the Samoan baby age problem, which cropped up on the old DevForums a few years ago. [QuinceyMorris was there too!]

Note Someone pointed out that not everyone can access the old DevForums, so I’ve included a summary of the problem below.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

[The question was: How do you calculate the age of a baby in days until they’re a week old, weeks until they’re a month old, and months until they’re a year old?]

As with most things related to dates and times, the more you think about it the harder it gets. There are two basic algorithms for calculating durations:

  • Take two UTC timestamps, diff them, divide (A)

  • Take two date components (era, year, month, day), diff them component-wise (B)

Normally you use B for anniversaries because A can result in weird side effects [1]. However, in the case of very short anniversaries, B can result in weird side effects as well. For example, consider the case of someone born at 23:15 (local time) in Samoa (UTC+14 in summer) who then takes 30 minutes to travel to American Samoa (UTC-11). The first timestamp translates to the components of yyyy/mm/dd 23:15. The second timestamp translates to the components of yyyy/mm/(dd-1) 22:45. If you diff the date components they are -1 days old!

Now, admittedly, travelling across time zones on your day of birth is pretty unusual but if you stretch the timeframe out to a week then the scenario is quite realistic: it’s perfectly reasonable for a person to travel from Samoa to American Samoa in the first week of their life, to see relatives and so on, and if that happens then it could be the difference between your app showing “6 days” and “one week” as their age.

Ultimately I don’t think this is an iOS-specific problem, but rather a problem of definition. I believe that, whatever you decide to show in this and other edge cases, iOS has APIs that can achieve that (-:

[1] For example:

  • People who change time zones get their ‘happy birthday’ notice on the wrong day

  • Don't get me started about people born on Feb 29 (-:

What if Maxwell goes to Tokyo and takes the hammer?

What if Maxwell goes to Tokyo and takes the hammer?

I’d argue that my first point is relative to the loaner, that is, the user who’s running your app, and thus you don’t care where the loanee ends up.

But your question speaks to a more general point: when dealing with dates and times you can always come up with ever more absurd edge cases. While I do want to encourage you to think this through in detail, I recognise that, at some point, you have to draw a line and decide that some edge cases just don’t matter.

Speaking personally, it’s very hard for me to draw that; I’m always striving towards a perfect solution. It’s both a blessing and curse (-:

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
Midnights between NSDates
 
 
Q