Search code examples
objective-ccocoa-touchcalendarnscalendar

Do I understand NSHebrewCalendar correctly?


I conducted a quick test with the Cocoa Touch to see how NSHebrewCalendar works. I'm particularly interested in the month numbers. I used a date picker to easily change dates, and I passed it in to a method which logs the hebrew date, the hebrew month number, the hebrew year, and if the year is leap year. That looks like this:

BOOL isHebrewLeapYear = [self.calendar isHebrewLeapYear:[calendar hebrewYearForDate:[self.calendar workingDate]]];

NSLog(@"Hebrew Date:%@, Month Number: %i, %i is Leap Year: %i", [self.calendar stringFromHebrewDate:[self.calendar workingDate]], [self.calendar hebrewMonthNumberFromDate:[self.calendar workingDate]], [calendar hebrewYearForDate:[self.calendar workingDate]], isHebrewLeapYear);    

The self.calendar object is a custom class. The workingDate property is an NSDate instance. Here are the relevant method declarations.

//  Check if a given year is a leap year
- (BOOL) isHebrewLeapYear:(NSInteger)year{
    return ((7 * year + 1) % 19) < 7;
}

//Get the hebrew year for a given date
- (NSInteger) hebrewYearForDate:(NSDate *)date{
   NSCalendar *hebrewCalendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSHebrewCalendar] autorelease];
   return [[hebrewCalendar components:NSYearCalendarUnit fromDate:date] year];
}

- (NSInteger) hebrewMonthNumberFromDate:(NSDate *)date{
    NSCalendar *hebrewCalendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSHebrewCalendar] autorelease];
    return [[hebrewCalendar components:NSMonthCalendarUnit fromDate:date] month];
}

Apparently hebrew leap years are handled as follows:

  • Month numbering starts at 1, with that month being "Tishri".
  • In a nonleap year, Adar is month number 7, not 6.
  • In a leap year, Adar I is number 6 and Adar II is 7.
  • "Nisan" is always 8 and so on until "Elul", which is always 13.

Does it sound like my experiment is producing accurate results? Is this behavior documented anywhere?


Solution

  • Month numbering starts at 1, with that month being "Tishri".

    Correct.

    In a nonleap year, Adar is month number 7, not 6.

    Technically incorrect. (More on this below)

    In a leap year, Adar I is number 6 and Adar II is 7.

    Correct.

    "Nisan" is always 8 and so on until "Elul", which is always 13.

    Correct.

    So what's up with Adar in non-leap years? I ran this code to find out:

    @autoreleasepool {
    
        NSDate *today = [NSDate date];
        NSCalendar *hebrew = [[NSCalendar alloc] initWithCalendarIdentifier:NSHebrewCalendar];
        NSDateComponents *diff = [[NSDateComponents alloc] init];
        NSDateFormatter *f = [[NSDateFormatter alloc] init];
        [f setDateFormat:@"d MMMM y"];
        [f setCalendar:hebrew];
    
        for (NSInteger i = 0; i < 19; ++i) {
            NSDateComponents *comp = [[NSDateComponents alloc] init];
            [comp setYear:5772 + i];
            [comp setDay:1];
    
            NSLog(@"============= %d ============", [comp year]);
    
            for (NSInteger i = 1; i <= 13; ++i) {
                [comp setMonth:i];
    
                NSDate *d = [hebrew dateFromComponents:comp];
                NSLog(@"%d: %@ (%@)", i, [f stringFromDate:d], d);
            }
        }
    
    }
    

    In a leap year, this will log what you would expect:

    ============= 5790 ============
    1: 1 Tishri 5790 (2029-09-10 07:00:00 +0000)
    2: 1 Heshvan 5790 (2029-10-10 07:00:00 +0000)
    3: 1 Kislev 5790 (2029-11-08 08:00:00 +0000)
    4: 1 Tevet 5790 (2029-12-07 08:00:00 +0000)
    5: 1 Shevat 5790 (2030-01-05 08:00:00 +0000)
    6: 1 Adar I 5790 (2030-02-04 08:00:00 +0000)
    7: 1 Adar II 5790 (2030-03-06 08:00:00 +0000)
    8: 1 Nisan 5790 (2030-04-04 07:00:00 +0000)
    9: 1 Iyar 5790 (2030-05-04 07:00:00 +0000)
    10: 1 Sivan 5790 (2030-06-02 07:00:00 +0000)
    11: 1 Tamuz 5790 (2030-07-02 07:00:00 +0000)
    12: 1 Av 5790 (2030-07-31 07:00:00 +0000)
    13: 1 Elul 5790 (2030-08-30 07:00:00 +0000)
    

    For each incrementation of the "month" date component, we get a different date. But when we run this in a non-leap year, we get this:

    ============= 5789 ============
    1: 1 Tishri 5789 (2028-09-21 07:00:00 +0000)
    2: 1 Heshvan 5789 (2028-10-21 07:00:00 +0000)
    3: 1 Kislev 5789 (2028-11-19 08:00:00 +0000)
    4: 1 Tevet 5789 (2028-12-19 08:00:00 +0000)
    5: 1 Shevat 5789 (2029-01-17 08:00:00 +0000)
    6: 1 Adar 5789 (2029-02-16 08:00:00 +0000)
    7: 1 Adar 5789 (2029-02-16 08:00:00 +0000)
    8: 1 Nisan 5789 (2029-03-17 07:00:00 +0000)
    9: 1 Iyar 5789 (2029-04-16 07:00:00 +0000)
    10: 1 Sivan 5789 (2029-05-15 07:00:00 +0000)
    11: 1 Tamuz 5789 (2029-06-14 07:00:00 +0000)
    12: 1 Av 5789 (2029-07-13 07:00:00 +0000)
    13: 1 Elul 5789 (2029-08-12 07:00:00 +0000)
    

    Here we see that have a month of 6 and 7 will both evaluate to Adar. Thus, Adar is both the 6th and the 7th month in non-leap years.


    Also, since we know that the year 5790 is a leap year, we can deduce a simpler implementation of the -isHebrewLeapYear: method:

    - (BOOL) isHebrewLeapYear:(NSInteger)year{
        return year % 19 == 14;
    }