An important lesson about NSCalendarComponents

I recently stumbled across some code that was so bad I had to comment on it.

//returns a Hebrew date from a Gregorian date
- (NSDate*)convertHebrewDateWithDate:(NSDate*)date {
...

When I saw this I was shocked! As you may already know an NSDate does not have any calendar information attached to it. It is a point in time. Internally it is a just a fancy wrapper for a count from January 1, 1970 2001 with a double precision floating point. So if you look at just the input and output of the function it makes no sense. You are inputing a NSDate (a point in time) and getting back another point in time.

Even more, there is no meaning to the words "Hebrew date" or "Gregorian date" all NSDates are calendar independent!

So what does the function actually do. Well nothing good, but lets take a look:

  NSCalendar* gergorianCalendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
  NSCalendar* hebrewCalendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSHebrewCalendar] autorelease];

It start off good making calendars which is often a good start when dealing calendar conversions. But then it is all down hill:

	NSUInteger dateUnits = NSDayCalendarUnit | NSMonthCalendarUnit | NSYearCalendarUnit;
	NSDateComponents *dateComponents = [hebrewCalendar components:dateUnits fromDate:date];

Then he creates a date Components from the Hebrew calendar. Something important to realize with NSDateComponents is that there is no context for what they mean. It is simply a collection of seconds, minutes, hours, days, weeks, months, and years. It could represent an amount of time - if for example they were created from NSCalendar's components:fromDate:toDate:options. Or they can represent numerical representations of calendar information as it does when it is created from NSCalendar's components:fromDate:. NSDateComponents does not reference the calendar it was created with to know what the information represents - that is up to the programmer. In the code above the components are a representation of a Hebrew Date. If, for example, you gave it a NSDate that was this year's Passover the components would be would be day=15, month=7 and year=5772.

This next line make me cringe a little:

	NSDate *hebrewDate = [gergorianCalendar dateFromComponents:dateComponents];
  return hebrewDate;
}

AHHHHH! You can't use Hebrew date components in a Gregorian calendar! This will create a date of July, 15, 5772 in the distant future (which by the way would be 18 Tammuz, 9532). It is completely meaningless!

The lessen here is already stated in a big prominent box in the Apple's documentation of NSDateComponents:

Important An NSDateComponents object is meaningless in itself; you need to know what calendar it is interpreted against, and you need to know whether the values are absolute values of the units, or quantities of the units.

Frankly I am not sure why NSDateComponents is an object instead of just a struct. If it was a struct it would be a lot clearer that it is not meant to do anything beyond simply storing values, and we wouldn't be seeing mistakes like this.

UPDATE: I also realized that the reverse conversion will not necessarily return you to the same date! In any Hebrew year where there isn't a leap year the 7th month is interpreted the same as a the 6th. So you could jump form July to Adar and then back to June.

Submit a Comment