NSCalendar’s quirks with regards to weeks

A former colleague of mine recently asked me a NSDate related question:

I'm struggling with a problem that should be simple.

The problem is how do I get the NSDate that represents "nearest Sunday at 00:00:00" (for example, in the Gregorian calendar).

So this is what I had in mind:

1. Create NSDateComponents from [NSDate date] against the gregorian calendar. Ask only for the year, the serial number of the week and the time.
2. Change the weekday of this date components to "1" and the time to 00:00:00.
3. Translate back to NSDate against the same calendar.

What's my problem:
What if this sunday is not in the same year? For example, if we are in the first week of the year, and the year started in Monday, what will I get if I'll ask for the "Sunday of week 1"?
Error? the right date? undefined?

This is a great question: How do years interact with weeks? While months and days never overlap two different years*, a week can start in on year and end in another! This brings us to the often overlooked NSDateComponent: NSYearForWeekOfYearCalendarUnit.

The YearForWeekOfYear has the property that it can start (or end) a little before or after the regular year, but will ensure that weeks do not straddle any year. How these "years" are exactly broken up is decided by two properties of NSCalendar: firstWeekday and minimumDaysInFirstWeek.

Now that were have a year system that guarantees that week will all be in the same year we can easily find the last Sunday:

-(NSDate*) LastSundayBeforeDate:(NSDate*) date{
    NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] ;
    NSUInteger unitFlags =NSWeekdayCalendarUnit | NSWeekOfYearCalendarUnit | NSYearForWeekOfYearCalendarUnit;
    NSDateComponents * components =[gregorian components:unitFlags fromDate:date];
    //1 is sunday in a gregorian calendar
    components.weekday=1;
    return [gregorian dateFromComponents: components];
}

Just like we can represent a date with {year-month-day}, we can also represent a date as {year-week-weekday} - as long as we remember that this is special kind of "year" where weeks are on only in one year. All we are doing is saying that for any date find the {year-week-weekday} then give me the date for {year-week-1}. This would be a very similar process if I wanted to find the date (NSDate) of the first day of a month that another date falls in.

Now to get next Sunday we would just need to add a week:

-(NSDate*) weekAfterDate:(NSDate*) date{
    NSDateComponents* oneWeekInterval=[[NSDateComponents alloc] init];
    oneWeekInterval.week=1;
    NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] ;
    return [gregorian dateByAddingComponents:oneWeekInterval toDate:date options:0];
}

Finally the function for next Sunday is just a week after last Sunday.

-(NSDate*) nextSunday:(NSDate*) date{
    return [self weekAfterDate: [self LastSundayBeforeDate:date]];
}

In the code above I am creating two different NSCalendars without any "real" benefit. Some would argue that the code should be refactored to be a single function. I disagree. Code clarity is more important than efficiency especially with objects that are already very fast.

In any event if you are calling this function over and over again, cutting the amount of NSCalendars in half is not the right answer. You should create an instance variable for the Gregorian Calendar and reuse it many many times.

There is one more thing to note. I said above that weeks can change years, unlike months which do not straddle years. This is not completely tree. The Japanese Calendar numbers the years from the start of the emperor’s reign. They normally change on December 31, but can change at any time of the year if the emperor dies.
■ 31 December, Showa 63
■ 1 January, Showa 64
■ …
■ 7 January, Showa 64
■ 8 January, Heisei 1

Submit a Comment