It's been a slow iPhone development week partly because I'm in the peak week of my marathon training but the main reason is that the work I did was pretty frustrating so it put me off doing more until today.
When is a date not a date?
When it's a datetime, silly. Working with dates in iPhone means you work with the misleading named class NSDate which not only stores a date and a time but the timezone data too. That little caveat aside I can dive into my issue dujour.
I'm building a UITableView where the groups are the week numbers and the cells are the days within those weeks calculated from a start date for the whole table. So if the start date is Monday September 21st 2009, the first section header is "week 1" and the first cell is Monday September 21st 2009.
The week section is easy to calculate:
NSUInteger section = [indexPath section];
NSUInteger row = [indexPath row];
NSUInteger currentWeek = section+1;
Next up is the calculation for the first cell. It should be easy right? The first cell of week 1 represents 0 weeks after the start date for the tableview and 0 days after the start, so our formula for dayOffset:
NSUInteger dayOffset = (section-1)*7+rows;
So day 1 of week 1 has a dayOffset of 0, day 1 of week 2 has a dayOffset of 7 etc. All that remains is to add the dayOffset to my startDate and I've got the date for the cell.
The challenge isn't finishing - starting is the hardest part
Working with an NSDate object my first instinct was to look at the NSDate class and find a function to add a set number of days to a date and found that there is only the function addTimeInterval:
- (id)addTimeInterval:(NSTimeInterval)seconds;
That's
seconds for those of you who missed the nuance. So it appears I have to turn my dayOffsets into seconds before I can add them to a start date. Fine.
NSUInteger secondsOffset = dayOffset*24*60*60;
First a quick side-bar. As I mentioned earlier, I'm passing in a date, not a date time so I set my date formatter object to parse MM/dd/yy dates (where MM is 2 digit month vs mm which is 2 digit minutes).
NSDateFormatter *df = [[NSDateFormatter alloc] init];
[df setDateFormat:@"MM/dd/yy"];
NSDate *frDate = [df dateFromString:[tableSourceFile objectForKey:@"startDate"]];
NSLog(@"%@",[frDate description]);
The NSLog confirms that my date "09/21/09" has now become "2009-09-21 00:00:00 -0500". This seems like a reasonable summary, I guess :-)
I added my dayOffset to my date:
NSDate *cellDate = [frDate addTimeInterval:secondsOffset];
and then as I drag down through my UITableView I see it all unfold perfectly.
Wait. Everything is fine but on Sunday November 1st 2009 instead of ticking over in Week 7 Day 1 to Monday November 2nd 2009 I get Sunday November 1st 2009 again. WTF?
I'm a little embarrassed at the hour and a half of bashing my head against this, double checking my start date, my logic, my days to seconds calculations, NSLogging out every variable involved to the log. It seems the first November 1st is "2009-11-01 00:00:00 -0500" and the second is "2009-11-01 23:00:00 -0600". A timezone switch? I plead late night development insanity on this but eagle-eyed wide-awake super-designer Jeff spots that it's not a change in timezone but the change in daylight savings time.
So turning days into seconds becomes nuts if you cross DST boundaries. To workaround this I have the bright idea to pass in noon as part of my start date because if the date shifts by 1 hour either way it still remains the same day:
NSDateFormatter *df = [[NSDateFormatter alloc] init];
//Change dateformatter to look for time
[df setDateFormat:@"MM/dd/yy HH:mm"];
NSDate *frDate = [df dateFromString:[tableSourceFile objectForKey:@"startDate"]];
//Correctly writes out "2009-09-21 12:00:00 -0500"
NSLog(@"%@",[frDate description]);
//Now do the addition:
NSDate *cellDate = [frDate addTimeInterval:secondsOffset];
I've created... a monster!
It works! By setting the time as noon I've got it working but I have a nagging feeling I'm missing something obviously simpler. It's close to midnight so I wrap up and leave for the night kicking myself for missing the obvious.
The next morning I pull up the docs and start digging around again looking in forums for date manipulations. I'm not sure of the exact path to enlightenment but I find the "Date and Time Programming Guide for Cocoa" and, more importantly, the section "Calendrical Calculations".
It *appears* that by using NSDateComponents to build an offset and NSCalendar to specify a basis for the calculations I can add the offset in days to a date based on the gregorian calendar. Since I'm adding days not seconds I wonder if this will avoid the issue I jammed thru.
Pressure creates diamonds
I didn't have time until this morning to test my theory so I sat down and plugged at it earlier.
//Create my dateformatter object
NSDateFormatter *df = [[NSDateFormatter alloc] init];
//Change dateformatter back to date only
[df setDateFormat:@"MM/dd/yy"];
NSDate *frDate = [df dateFromString:[tableSourceFile objectForKey:@"startDate"]];
//NSLog confirms my date"2009-09-21 00:00:00 -0500"
NSLog(@"%@",[frDate description]);
//Create an offset object
NSDateComponents *offsetComponents = [[NSDateComponents alloc] init];
//Pass in our offset in days
[offsetComponents setDay:(section*7)+row];
//Select a calendar for the calculations
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
//With the gregorian calendar add my offset object to the start date
NSDate *cellDate = [gregorian dateByAddingComponents:offsetComponents toDate:frDate options:0];
//Release the offset and the calendar
[offsetComponents release];
[gregorian release];
Week 1 Day looks fine and dandy "2009-09-21 00:00:00 -0500", what about the mysterious DST cutover on Week 6 day 7? "2009-11-01 00:00:00 -0500" and then Week 7 Day 1: "2009-11-02 00:00:00 -0600". It behaves the way I expected so when I output my date to the UITableViewCell textLabel I get the day and can ignore the time:
//Display 3 character day of week, 2 digit day and 3 character month e.g. Mon, 21 Sep
[df setDateFormat:@"EEE, dd MMM"];
cell.textLabel.text = [edf stringFromDate:today];
Voila. 3 hours of development time to add a number of days to a date because my first instinct was such a wild goose chase. I've emailed the iPhone docs team to let them know that it would be great to add the link to the section on "Calendar Calculations" to the addTimeInterval method detail since, IMHO, it's the first place a new iPhone/cocoa developer would think to look.
I hope this helps someone else dodge the bullet and let me know if there are typos/errors/omissions in the code since typing in anger like this rarely reproduces perfect code! Have a great weekend.