8

I am using the following NSCalendar method to get the number of days in a calendar year:

NSRange range = [gregorian rangeOfUnit:NSDayCalendarUnit 
                                inUnit:NSYearCalendarUnit 
                               forDate:date];

I am expecting a range.length return value of type NSRange which is 365 or 366 (days).

However, this code returns a range.length of 31 for a date value of 2005-06-06.

What is wrong with my code?

This is the full code snippet:

NSCalendar *gregorian = [[NSCalendar alloc]
initWithCalendarIdentifier:NSGregorianCalendar];

[subArray enumerateObjectsUsingBlock:
   ^(NSDate *date, NSUInteger idx, BOOL *stop) {
   NSUInteger numberOfDays = [gregorian ordinalityOfUnit:NSDayCalendarUnit
   inUnit:NSYearCalendarUnit forDate:date];
}];
paulmelnikow
  • 16,895
  • 8
  • 63
  • 114
AlexR
  • 5,514
  • 9
  • 75
  • 130
  • You will always get 31 for any of the month. – Anoop Vaidya Feb 18 '13 at 16:50
  • Yes I tried same with any year, any month it always showed 31. Once I changed NSMonthCalenderUnit days count were correct.!!! Either we are missing something or this API has some bugs. – Anoop Vaidya Feb 18 '13 at 16:55
  • 2
    The method works correct. It computes the range of possible values for a unit in the timespan of a larger unit. Each year contains a month that has 31 days. – Nikolai Ruhe Feb 18 '13 at 16:57

4 Answers4

2

This calculates the number of days of a year of a given date:

NSDate *someDate = [NSDate date];

NSDate *beginningOfYear;
NSTimeInterval lengthOfYear;
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
[gregorian rangeOfUnit:NSYearCalendarUnit
             startDate:&beginningOfYear
              interval:&lengthOfYear
               forDate:someDate];
NSDate *nextYear = [beginningOfYear dateByAddingTimeInterval:lengthOfYear];
NSInteger startDay = [gregorian ordinalityOfUnit:NSDayCalendarUnit
                                          inUnit:NSEraCalendarUnit
                                         forDate:beginningOfYear];
NSInteger endDay = [gregorian ordinalityOfUnit:NSDayCalendarUnit
                                        inUnit:NSEraCalendarUnit
                                       forDate:nextYear];
NSInteger daysInYear = endDay - startDay;

Sad caveat: This does not work correctly for year 1582.

The year 1582, the year when Gregor introduced the currently widespread used calendar, needed a fix to align solar with calendar years. So they went with the pragmatic solution: They just dropped October 5-14. (They were not crazy enough to change weekdays, too). As a result the year 1582 only has 355 days.

Addendum: The code above only works correctly for years after 1582. It returns 365 days for the year 1500, for example, even though this year was a leap year in the then used julian calendar. The gregorian calendar starts at October 15, 1582. Computations made on the gregorian calendar are just not defined before that date. So in this way Apple's implementation is correct. I'm not aware of a correct implementation for years before 1583 on Cocoa.

Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
  • Just out of curiosity, does Cocoa's Gregorian implementation also account for the vagaries of the gradual country-by-country adoption of the calendar? For example, France was two months behind Italy and Spain in adoption, and Sweden did something like skipping leap days for a century rather than jumping a week at once. – jscs Feb 18 '13 at 18:28
  • @JoshCaswell As far as I can tell this is not even modeled: You cannot ask for a calendar for a specific region or date. So I guess the gregorian calendar just works as is. You can ask it for a leap year before it is defined and it would answer as if the gregorian calendar would have been in effect at that time. – Nikolai Ruhe Feb 18 '13 at 18:37
  • 2
    It's *entirely* consistent: It uses the "proleptic Gregorian calendar", which roughly means "relabel the days working backwards, using the same rules". Thus 1500 is not a leap year but 1200 is (along with 0000, and please don't start the year 0 debate). Care must be taken to avoid misinterpretation, but it's what ISO 8601 specifies, what astronomers use, and what historians convert to to make calculations easier (often with the suffix "NS"). – tc. Feb 18 '13 at 18:42
  • @tc. Thanks for the clarification. Great info provided! – Nikolai Ruhe Feb 18 '13 at 18:47
  • Ah-ha! Then ISO _is_ a part of the papists' New World Order plot! (joke!) Thanks for the info, Nikolai and @tc. – jscs Feb 18 '13 at 18:55
  • @tc. The documentation seems to contradict your statement: "NSCalendar models the transition from the Julian to Gregorian calendar in October 1582. During this transition, 10 days were skipped. This means that October 15, 1582 follows October 4, 1582. All of the provided methods for calendrical calculations take this into account". See http://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/DatesAndTimes/Articles/dtHist.html#//apple_ref/doc/uid/TP40010240-SW4 – Nikolai Ruhe Feb 18 '13 at 19:41
  • @NikolaiRuhe: Nice catch! This seems to be a difference between iOS and OS X (and an error in the iOS SDK docs): In iOS, NSCalendar ignores the discontinuity and the different leap year rule. OTOH, on both platforms, NSCalendar represents BC dates using "era 0", i.e. 100 BC = "era 0, year 100" (and "0 AD" = 1 BC = "era 0, year 1); this gets *really* tricky because "adding a year" to e.g. 100 BC results in 101 BC (time goes backwards!), causing the code above to break. The leap year rule is extended such that 1 BC and 401 BC are leap years (but 101 BC is a leap year on OS X but not iOS). – tc. Feb 18 '13 at 20:44
  • Tangentially, `CFGregorianDate` and its related functions seem to reliably use the proleptic Gregorian calendar; useful for ISO 8601 or if you just want to avoid discontinuities, changing leap year rules, and special-casing of BC dates. – tc. Feb 18 '13 at 20:52
  • @tc. Well my observations seem to tell a different story, though. I do get 365 days for the year 1582 (not the 355 days the documentation seems to promise). Also I can create dates from components in the range 5 to 14 Oct. 1582. – Nikolai Ruhe Feb 18 '13 at 20:58
  • @NikolaiRuhe: What are you testing on? I used 10.7.5 and the iOS 6 simulator; on 10.7.5 I get 355 days in 1582 and 366 in 1500. – tc. Feb 18 '13 at 21:00
  • I'm on 10.8.2 (testing on Foundation only, no simulator). – Nikolai Ruhe Feb 18 '13 at 21:01
  • Without further testing, my guess is that they changed it in 10.8 (and iOS 6.0, if it was ever supported in iOS). – tc. Feb 20 '13 at 01:23
1

What about finding number of days in a given year as: Although this is not the relative solution for the question.

-(NSInteger)daysBetweenTwoDates:(NSDate *)fromDateTime andDate:(NSDate *)toDateTime{

    NSDate *fromDate;
    NSDate *toDate;

    NSCalendar *calendar = [NSCalendar currentCalendar];

    [calendar rangeOfUnit:NSDayCalendarUnit startDate:&fromDate interval:NULL forDate:fromDateTime];
    [calendar rangeOfUnit:NSDayCalendarUnit startDate:&toDate interval:NULL forDate:toDateTime];

    NSDateComponents *difference = [calendar components:NSDayCalendarUnit fromDate:fromDate toDate:toDate options:0];

    return [difference day]; 
}

-(void)someMethod {

    NSString *yourYear=@"2012";

    NSDate *date1 = [NSDate dateWithString:[NSString stringWithFormat:@"%@-01-01 00:00:00 +0000",yourYear]];
    NSDate *date2 = [NSDate dateWithString:[NSString stringWithFormat:@"%@-12-31 00:00:00 +0000",yourYear]];

    NSInteger numberOfDays=[self daysBetweenTwoDates:date1 andDate:date2];    

    NSLog(@"There are %ld days in between the two dates.", numberOfDays);
}            

Edit:

As dateWithString: is deprecated, you can use

NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
[dateFormatter dateFormat]=@"dd-MM-yyyy";
NSDate *date=[dateFormatter dateFromString:dateString];

to form date1 and date2

Anoop Vaidya
  • 46,283
  • 15
  • 111
  • 140
1

Here's a relatively clean version. It's a category on NSCalendar.

- (NSInteger) daysInYear:(NSInteger) year {
    NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
    dateComponents.year = year;
    dateComponents.month = dateComponents.day = 1;
    NSDate *firstOfYear = [self dateFromComponents:dateComponents];
    dateComponents.year = year + 1;
    NSDate *firstOfFollowingYear = [self dateFromComponents:dateComponents];

    return [[self components:NSDayCalendarUnit
                   fromDate:firstOfYear
                     toDate:firstOfFollowingYear
                     options:0] day];    
}

Thanks to AKV for pointing out that -components:fromDate:toDate:options will work in this case.

This doesn't seem to work for 1582 or prior, which per Nikolai Ruhe's answer is because Gregorian calendar calculations simply aren't defined prior to then.

paulmelnikow
  • 16,895
  • 8
  • 63
  • 114
0

You are getting the number of days in the month of your date.

You could use the same function and just pass in a date with February as the month. If range.length returns 28, the Total days in the year will be 365, if it returns 29, then the number of days will be 366.

Adam Johnson
  • 2,198
  • 1
  • 17
  • 23