Mastodon

Dates and Data Models

Just use NSDate, right?

The obvious choice for handling dates in iOS and OS X apps is NSDate. It’s obvious, right? It’s got “date” right in its name, doesn’t it?

And that’s the thing about NSDate. It’s mis-named. It almost says so right in the docs:

NSDate objects encapsulate a single point in time, independent of any particular calendrical system or time zone.

A single point in time? I don’t know about you but I think of “date” as meaning more like

[T]he day of the month or year as specified by a number: what’s the date today? | please give your name, address, and date of birth. [source: OS X’s dictionary]

And there you have it. NSDate does not represent a date, at least not as the word “date” is commonly understood. So what, basically, the hell is it?

That line from the docs pretty much says it, though the implications might not be obvious. An NSDate is a single instant in time, down to the fraction of a second. The class is an object interface to a single floating point number representing the number of seconds since a reference point in time. Why it’s called NSDate is a question to which I have no answer.

Saying it’s independent of any calendrical system is a fancy way of saying, basically, this is not actually a date in any rational sense of the word. NSDate won’t help you find out the date associated with its value. You can use NSDateFormatter to get a human-readable date, but often that’s still not really what you need.

Time Zones Rear their Ugly Head

Mentioning that NSDate is independent of a time zone is a key point of understanding. People often make the mistake of wondering what time zone an NSDate is in, or worse, how to convert an NSDate from one time zone to another. It has no time zone. If two people in different time zones call NSDate() at exactly the same moment, they’ll get exactly the same result.

Zones come in because they’re a necessary part of getting from an NSDate to an actual date. As of just a moment ago, NSDate().timeIntervalSinceReferenceDate gives me a floating point value of 477959641.90769. So what date is that? Where I’m sitting, it’s 23 February, 2016. If I were sitting in Singapore, I’d get the same value from NSDate but the date would be 24 February, 2016. So what date does that NSDate represent? It depends what time zone you’re in.

That’s fine as far as it goes, and it’s more than sufficient for a lot of things. If you just want to know when something happened, NSDate is there for you. If you want to know if thing A happened before or after thing B, NSDate has got your back. But what if you want to know everything that happened on 23 February 2016?

Now all of a sudden NSDate isn’t looking so useful. Strictly speaking, you’ll have to do something like

  1. Pick a time zone that seems appropriate
  2. Work out the NSDate corresponding to the beginning of 23 Feb in that time zone
  3. Work out the NSDate corresponding to the end of 23 Feb in that time zone
  4. Filter your data using these two floating point values.

None of this is impossible but it’s more complex than it feels like it should be. It can also lead to unexpected results. Maybe I’m using a journalling app, and I create a couple of entries on 23 Feb. Then I fly to Singapore. And now the app shows that those entries were created on 24 Feb while none were created on the 23rd, because the creation date is an NSDate. Except I know I didn’t create those entries on the 24th because I was writing on the 23rd about an event that happened that day. Your app is now strictly correct but, from my perspective, wrong. But hey, NSDate, right? Whaddya gonna do?

It gets worse. What if you need to group your data by the week of the year? You’d have to repeat the steps above for every possible week covered by the data.

Store data that you actually need

Cases like these are why it’s important to make sure you’re storing the data you’ll actually need later on instead of just adding an NSDate and thinking you’re done.

What that is depends on how you think you’ll need to use the data. If you’ll be searching, filtering, or grouping your data by date components, include fields to represent those components in your data model. If it’s a date that should remain fixed, you probably want day, month, and year. If you’re grouping by week, store the week of the year as its own field. Anything from NSDateComponents is a likely choice depending on what your app does. You probably don’t want NSDateComponents itself though. It conforms to NSCoding for easy archiving, of course. But once archived you can’t use it for searching, etc, without de-archiving first.

A trivial journal-entry model struct might look like this:

struct JournalEntry {
    var text: String
    var year: Int
    var month: Int
    var day: Int
    
    init(text: String) {
        self.text = text
        
        let dateComponents = NSCalendar.currentCalendar().components([.Year, .Month, .Day], fromDate: NSDate())
        self.year = dateComponents.year
        self.month = dateComponents.month
        self.day = dateComponents.day
    }
}

If the only important consideration is displaying a consistent date to the user, you might be fine using NSDateFormatter to create a string representation of the date, and saving just that string. You can’t easily sort or filter data with that, but if you don’t care about those things then don’t save data for them. In that case the trivial journal entry might be represented as

struct JournalEntry {
    var text: String
    var dateString: String
    
    init(text: String) {
        self.text = text
        
        let formatter = NSDateFormatter()
        formatter.dateStyle = .LongStyle
        formatter.timeStyle = .NoStyle
        self.dateString = formatter.stringFromDate(NSDate())
    }
}

You might still want to include an NSDate. They’re handy for sorting objects quickly, since ordering only considers a single numeric value. But if you need something else, be sure to include that as well.

Core Data vs. Dates

All of the above applies when using Core Data as well. The only detail to be aware of is that Core Data’s Date type just means you’re reading and writing NSDate. If NSDate isn’t quite what you need, neither is Date.