Posts Tagged ‘NSDateComponents’

NSDates and Fractional Seconds

Wednesday, July 18th, 2012

A recent overhaul to Wooly Tasks allowed me to manage and query for Task records quicker, more easily, and more reliably. However, I quickly ran into a silly snap when ordering lists of Tasks based on their due dates.

Would you care for another date?…

The recommended method for adding time to a NSDate is by setting up a NSDateComponents object and adding the components to the date object that you want to change. Behind the scenes, the OS will handle the cases of Daylight Savings time changes and Leap Years correctly. Simply adding a NSTimeInterval does not.

Incorrect:

NSTimeInterval oneHour = 3600; // magic number! 60 seconds * 60 minutes
NSDate *newDate = [date dateByAddingTimeInterval:oneHour];

Correct:

NSDateComponents *components = [NSDateComponents new];
[dateComponents setHour:1];
NSDate *newDate = [[NSCalendar currentCalendar] dateByAddingComponents:components toDate:date options:0];

The NSDateComponents class interface for setting the hour uses a NSInteger, and not a float or double. This is true of all the other components:

...
- (void)setDay:(NSInteger)v;
- (void)setHour:(NSInteger)v;
- (void)setMinute:(NSInteger)v;
...

Dates gone bad…

OK, now we know the correct way to add time to a date, let’s look at something that could bite us in the ass. In the case of Wooly Tasks, we limit due dates to have granularity of every 5 minutes. We also prepopulate a new task with a date that falls on the hour, and is at least 30 minutes from the moment the task was created. So if the current time is 12:34 when we create the task, then we’ll choose 2:00 instead of 1:00 as the due date time. We call this normalizing the due date.

The problem exists, if we create a couple of tasks and normalize the date something like:

NSDate *date = [NSDate date];
NSUInteger unitFlags = NSMinuteCalendarUnit+NSSecondCalenderUnit;
NSDateComponents *components = [[NSCalendar currentCalender] components:unitFlags fromDate:date];
NSInteger hour = ([components minute]<30) ? 1 : 2;
[components setHour:hour];
[components setMinute:-[components minute]];
[components setSecond:-[components second]];
date = [[NSCalendar currentCalendar] dateByAddingComponents:components toDate:date options:0];

This will wind the minutes and seconds back to hh:00:00, and the hour ahead by one (two if the date was less than 30 minutes from the next whole hour). Unless you examine the actual NSTimeInterval of these dates, then several created within a short period of time (for our example, within 5.5 seconds of each other) that might appear to be the same time:

Date 1:

July 18, 2012 4:31:39 PM PDT
364347099.942335
July 18, 2012 6:00:00 PM PDT
364352400.942335

Date 2:

July 18, 2012 4:31:45 PM PDT
364347105.456080
July 18, 2012 6:00:00 PM PDT
364352400.456080

Each date above is shown with four values: the raw date, the raw date in seconds*, the normalized date, and the normalized date in seconds*. (* number of seconds since January 1st, 2001 GMT). What you notice is that the seconds display have a fractional part that doesn’t get reflected by the user readable display.

This becomes problematic in applications that want to sort records by dates as the primary sort key, and another criteria for a secondary sort key. Particularly so when the coarseness of the dates is less than at the seconds level. For instance, if we sorted the above normalized dates, then record with Date 2 would appear before record with Date 1, even though to the user they would appear to be the same. In cases where the secondary sort criteria would have put a record with Date 1 before Date 2, this sorting would have failed to do so.

A Good Date…

There is no way to remove these fractional seconds by using -dateByAddingComponents:toDate:options: because -setMinute:, as noted above, accepts a NSInteger and not a floating point value type. We can easily modify our code above to handle that using our handy-dandy function, trunc():

NSDate *date = [NSDate date];
NSTimeInterval seconds = trunc([date timeIntervalSinceReferenceDate]);
date = [NSDate dateWithTimeIntervalSinceReferenceDate:seconds];
NSUInteger unitFlags = NSMinuteCalendarUnit+NSSecondCalenderUnit;
NSDateComponents *components = [[NSCalendar currentCalender] components:unitFlags fromDate:date];
NSInteger hour = ([components minute]<30) ? 1 : 2;
[components setHour:hour];
[components setMinute:-[components minute]];
[components setSecond:-[components second]];
date = [[NSCalendar currentCalendar] dateByAddingComponents:components toDate:date options:0];