Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2016-06-16

NSDate, NSCalendar and NSDateComponents

The Cocoa framework provides excellent support for working with date's. But -though not too high- the learning curve is more of a step function. The following may help:

NSDate: A moment in time. Don't think of NSDate as a date because it is not. A NSDate simply represents a moment in time.

NSCalendar: This is mental abstraction of time. Humans like to think of time in a certain way. The most common being the gregorian calendar. I.e. year, month, day, starting the year at January the 1st and ending it on December the 31st.

In our code we often need a "moment in time", i.e. an NSDate. A computer does not need mental abstractions and thus works with an internal representation of time. Which one is usually not all that important. Suffice to say that NSDate is such an internal representation. It does not matter where you are -or where your users are- the NSDate is the same for all of us.

However as soon as user interaction is needed we must convert to and from the mental abstraction of time that the user uses. For that we need to convert the NSDate to a year, month and day. Enter NSDateComponents.

NSDateComponents is a container for units that the user knows and understands: Year, month, day (and more). And to be sure, not just the end user, also the programmer ;-)

To convert a NSDate into its NSDateComponents we need a NSCalendar. You can see quite some code out there that uses a NSDateFormatter for this, but don't do that. Its cumbersome and the code is usually not that pretty or maintainable. And that is on top of conversion problems between calendars that could easily result in your code not being portable between calendar regions.

The class NSCalendar offers us a "currentCalendar" that is set to the user's preferences. This is usually the easiest way to be sure that the user will understand the GUI representation of a NSDate.

Lets take an easy example:

let now = NSDate() // Fix a moment in time
let calendar = NSCalendar.currentCalendar() // A reference to the calendar the user wants to use
let components = calendar.components(NSCalendarUnit(arrayLiteral: .Year, .Month, .Day), fromDate: now)


print("The numerical representation of now is \(components.year)-\(components.month)-\(components.day)")

Which outputs for the time of writing:

The numerical representation of now is 2016-6-16

Of course if we really wanted to create a user readable string, we could use a NSDateFormatter instead. However if we wanted to store an internal representation of the moment-in-time "now" it is much more efficient to use the "components" than do some magic around NSDateFormatter.

In a previous post I created a WallclockTime class. I did in fact also create a YearMonthDay class for my project. However I have seen the error in my way's and no longer use these. Instead I now use NSDateComponents where I need to associate a moment-in-time with user data or activities.

This results in some extensions to NSDate and NSDateComponents:

extension NSDate {
    
    func yearMonthDay(calendar: NSCalendar? = nil) -> NSDateComponents {
        let calendar = calendar ?? NSCalendar.currentCalendar()
        let components = calendar.components(NSCalendarUnit(arrayLiteral: .Year, .Month, .Day), fromDate: self)
        return components
    }
    
    func hourMinuteSecond(calendar: NSCalendar? = nil) -> NSDateComponents {
        let calendar = calendar ?? NSCalendar.currentCalendar()
        let components = calendar.components(NSCalendarUnit(arrayLiteral: .Hour, .Minute, .Second), fromDate: self)
        return components
    }
}

extension NSDateComponents {
    
    var json: VJson {
        let j = VJson.createObject(name: nil)
        if self.year != NSDateComponentUndefined { j["Year"].integerValue = self.year }
        if self.month != NSDateComponentUndefined { j["Month"].integerValue = self.month }
        if self.day != NSDateComponentUndefined { j["Day"].integerValue = self.day }
        if self.hour != NSDateComponentUndefined { j["Hour"].integerValue = self.hour }
        if self.minute != NSDateComponentUndefined { j["Minute"].integerValue = self.minute }
        if self.second != NSDateComponentUndefined { j["Second"].integerValue = self.second }
        return j
    }

    convenience init?(json: VJson?) {
        guard let json = json else { return nil }
        self.init()
        if let jval = (json|"Year")?.integerValue { self.year = jval }
        if let jval = (json|"Month")?.integerValue { self.month = jval }
        if let jval = (json|"Day")?.integerValue { self.day = jval }
        if let jval = (json|"Hour")?.integerValue { self.hour = jval }
        if let jval = (json|"Minute")?.integerValue { self.minute = jval }
        if let jval = (json|"Second")?.integerValue { self.second = jval }
    }

}

Do note that the calendar is not set on the convenience init. Hence the resulting NSDateComponents object is useful for little else than simply storage and usage in calendar operations.

This highlights an important restriction of NSDateComponents: it is a rather 'passive' class. It provides storage and does little else. Once it has been created the contents is pretty much static, even adding a calendar to it later does not change this. Once a component is set it stay's that way until we ourself set it to a suitable value.

PS: VJson is a class that you can find in SwifterJSON (on the right hand side)

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment