views:

79

answers:

3

Greetings,

Recently I faced a big problem (as it seems to me) with NSCalendar class.

In my task I need to work with a large time periods starting from 4000BC to 2000AD (Gregorian calendar). In some place I was forced to increment some NSDate by 100 year interval. When incrementing the years in AD timeline (0->...) everything worked fine, but when I tried the same thing with BC i was a little confused.

The problem is, when you try to add 100 years to 3000BC [edited] year, you get 3100BC [edited] no matter what... Personally i found it strange and illogical. The right result should be 2900BC.

Here is the code sample for you to see this "not right" behavior:

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

// initing
NSDateComponents *comps = [[[NSDateComponents alloc] init] autorelease];
[comps setYear:-1000];
NSDate *date = [gregorian dateFromComponents:comps];

// math
NSDateComponents *deltaComps = [[[NSDateComponents alloc] init] autorelease];
[deltaComps setYear:100];

date = [gregorian dateByAddingComponents:deltaComps toDate:date options:0];

// output
NSString *dateFormat = @"yyyy GG";

NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:dateFormat];
NSLog(@"%@", [formatter stringFromDate:date]);

What can you say about this behavior? Is this how it should work or is this a bug? I'm confused :S.

BTW.: the method [NSCalendar components:fromDate:toDate:options:] doesn't allow us to calculate the difference between years in BC era... additional 'WHY?' in this Pandora's box.

P.S.: I was digging through official documentation and other resources but found nothing regarding this problem (or maybe it's intended to work so and I'm an idiot?).

A: 

Imagine that you have Date with 1st moment of our era - AD 0001-01-01 00:00:00. What was the moment before? BC 0001-01-01 00:00:01. If Cocoa developers used basic arithmetics for this task, you would get AD 0000-12-31 23:59:59. Is that reasonable for Gregorian calendar? I guess not. So, it seems to me that the most convenient way to implement calendar was to use Era flag, and change "time direction" when dealing with BC era to get human-readable dates in every case.

BTW.: [NSCalendar dateByAddingComponents:toDate:options:] really behaves strange and is unable to count time interval between BC dates, I checked too. So, for BC dates you may use workaround, e.g. by translating dates to AD and then finding diff.

parametr
Actually, the second before is BC 0001-12-31 23:59:59. Only the year numbers run backwards.
Boaz Stuller
The basic arithmetics works fine in NSCalendar (except the case i described). When you subtract 100 years from 50AD, you get 51BC. But is seems 'inverted' when you deal with BC dates.
GregoryM
+1  A: 

It's a bug and or a feature. The Apple doc never says what they mean by adding components to the calendrical date. It's perfectly free for them to define "adding a component" to the BCE date as just the addition to the year component.

Yes I agree with you that it's counterintuitive and I think it's a bug.

You need to convert your NSDate to either

  • the second from the UNIX epoch (1.1.1970) using -timeIntervalSince1970
  • the second from the OS X epoch (1.1.2001) using -timeIntervalSinceReferenceDate

You can then perform the calculation, and convert it back to an NSDate. I think it's a bad idea to work in the Gregorian calendar all the time... It would be better to convert to the Gregorian calendar just before you show it on the GUI.

Yuji
Your idea is actually right, but there is a problem. When you are trying to add 100 years to specific NSDate, you use [NSCalendar dateByAddingComponents:...], why? Because you don't know how many seconds it this 100 years exactly, right? And as I pointed before, the method [NSCalendar components:fromDate:toDate:options:] doesn't work in BC era at all, so there is no chance of getting the exact seconds count for 100 years, is there?
GregoryM
If you really need that specific operation, I guess you need to code them yourself. Honestly I don't understand what you mean by "add 100 years exactly". What's 100 years plus "400AD Feb. 29"? You need to make some ad-hoc rule to deal with the leap years anyhow.
Yuji
I think adding 100 years to some date isn't so 'specific' operation :) and that I should write my own methods to do this. I was just wondering about how untested the apple classes are. Afaik unit tests were invented far too long ago...
GregoryM
A: 

I found a simple workaround for this bug. Here it is:

@interface NSCalendar (EraFixes)

- (NSDate *)dateByAddingComponentsRegardingEra:(NSDateComponents *)comps toDate:(NSDate *)date options:(NSUInteger)opts;

@end

@implementation NSCalendar (EraFixes)

- (NSDate *)dateByAddingComponentsRegardingEra:(NSDateComponents *)comps toDate:(NSDate *)date options:(NSUInteger)opts
{
    NSDateComponents *toDateComps = [[self components:NSEraCalendarUnit fromDate:date] autorelease];
    NSDateComponents *compsCopy = [[comps copy] autorelease];

    if ([toDateComps era] == 0) //B.C. era
    {
        if ([comps year] != NSUndefinedDateComponent) [compsCopy setYear:-[comps year]];
    }

    return [self dateByAddingComponents:compsCopy toDate:date options:opts];
}

@end

If you wonder why I invert only years, the answer is simple, every other component except years is incrementing and decrementing in the right way (I haven't tested them all, but months and days seem to work fine).

GregoryM