NSCalendar date calculation anomaly

  • I have an int representing the number of days since 1/1/2001, and wish to get an NSDate representing that date in the system's local time zone:

    NSDateComponents *dc = [[[NSDateComponents alloc] init] autorelease];
    [dc setDay: numdays];
    NSDate * cd = [[NSCalendar currentCalendar] dateByAddingComponents: dc toDate: [NSDate dateWithString: @"2001-01-01"] options: 0];
    NSLog( @"date 1: %@", [cd description] );
    [dc setDay: 0];
    [dc setSecond: -[[NSTimeZone systemTimeZone] secondsFromGMTForDate: cd]];
    cd = [[NSCalendar currentCalendar] dateByAddingComponents: dc toDate: cd options: 0];
    NSLog( @"date 2: %@", [cd description] );

    If for example numdays is the number of days from 1/1/2001 to 5/4/2010, then the output is this:

    2010-04-27 21:33:55.803 PedCard[95387:a0f] date 1: 2010-05-03 17:00:00 -0600
    2010-04-27 21:33:55.804 PedCard[95387:a0f] date 2: 2010-05-03 23:00:00 -0600

    And, BTW, my current time zone is MDT, so -0600 is not correct, it should be -0700. So what's going on? I would expect those dates to be:

    2010-05-03 17:00:00 -0700
    2010-05-04 00:00:00 -0700

    It seems as though some calls are using one time zone and some are using another??? I do not do anything in my app to set a local or default time zone, and I have not changed the time zone in my system preferences in a very long time. (Multiple reboots ago.) Add to that, the code did produce the correct date a few times earlier tonight... Also, it doesn't seem to make a difference whether I use systemTimeZone or localTimeZone.

    OS X 10.6.3, Xcode 3.2.2

    --
    Scott Ribe
    <scott_ribe...>
    http://www.elevated-dev.com/
    (303) 722-0567 voice
  • Digging under the covers:

    CFGregorianUnits gu = { 0, 0, numdays, 0, 0, 0 };
    CFAbsoluteTime at = CFAbsoluteTimeAddGregorianUnits( 0, NULL, gu );
    cd = [NSDate dateWithTimeIntervalSinceReferenceDate: at];
    NSLog( @"date 4: %@", [cd description] );

    Produces: "date 4: 2010-05-04 18:00:00 -0600", which is at least the right moment in time, even though it is not displayed correctly.

    While trying to use the time zone explicitly:

    CFGregorianUnits gu = { 0, 0, numdays, 0, 0, 0 };
    CFAbsoluteTime at = CFAbsoluteTimeAddGregorianUnits( 0, CFTimeZoneCopySystem(), gu );
    cd = [NSDate dateWithTimeIntervalSinceReferenceDate: at];
    NSLog( @"date 4: %@", [cd description] );

    Produces: "date 4: 2010-05-04 17:00:00 -0600", which is wronger ;-)

    --
    Scott Ribe
    <scott_ribe...>
    http://www.elevated-dev.com/
    (303) 722-0567 voice
  • On Tue, Apr 27, 2010 at 8:45 PM, Scott Ribe <scott_ribe...> wrote:
    > NSDate * cd = [[NSCalendar currentCalendar] dateByAddingComponents: dc toDate: [NSDate dateWithString: @"2001-01-01"] options: 0];

    This is a wrong. You don't need to go from a string to a date here.
    You are violating the requirement laid out in the documentation: "You
    must specify all fields of the format string, including the time zone
    offset, which must have a plus or minus sign prefix." And if you ever
    want to convert a string to a date, you should be using an
    NSDateFormatter anyway.

    So don't do this. Just use -initWithTimeIntervalSinceReferenceDate:
    instead. The reference date is conveniently 2001-01-01 00:00:00 +0000.

    --Kyle Sluder
  • On Apr 27, 2010, at 10:37 PM, Kyle Sluder wrote:

    > So don't do this. Just use -initWithTimeIntervalSinceReferenceDate:
    > instead.

    Been there, done that, same problem. (In fact the reason I switched to dateWithString: was to play around with the time zone offset when constructing the date.)

    --
    Scott Ribe
    <scott_ribe...>
    http://www.elevated-dev.com/
    (303) 722-0567 voice
  • Le 28 avr. 2010 à 05:45, Scott Ribe a écrit :

    >
    > It seems as though some calls are using one time zone and some are using another??? I do not do anything in my app to set a local or default time zone, and I have not changed the time zone in my system preferences in a very long time. (Multiple reboots ago.) Add to that, the code did produce the correct date a few times earlier tonight... Also, it doesn't seem to make a difference whether I use systemTimeZone or localTimeZone.
    >

    Did you try to use NSDateComponents for building your reference date (2001/01/01) such as :

    NSCalendar *cal = [NSCalendar currentCalendar];
    NSDateComponents *refDateComps = [[NSDateComponents alloc] init];
    [refDateComps setYear:2001];
    [refDateComps setMonth:01];
    [refDateComps setDay:01];
    [refDateComps setHour:0];
    [refDateComps setMinute:0];
    [refDateComps setSecond:0];

    NSDate *refDate = [cal dateFromComponents:refDateComps];
    [refDateComps release];

    NSDateComponents *dc = [[[NSDateComponents alloc] init] autorelease];
    [dc setDay: numdays];
    NSDate * cd = [cal dateByAddingComponents: dc toDate: refDate options: 0];

    K.
  • On Apr 27, 2010, at 10:45 PM, Scott Ribe wrote:

    > I have an int representing the number of days since 1/1/2001, and wish to get an NSDate representing that date in the system's local time zone:
    >
    > NSDateComponents *dc = [[[NSDateComponents alloc] init] autorelease];
    > [dc setDay: numdays];
    > NSDate * cd = [[NSCalendar currentCalendar] dateByAddingComponents: dc toDate: [NSDate dateWithString: @"2001-01-01"] options: 0];
    > NSLog( @"date 1: %@", [cd description] );
    > [dc setDay: 0];
    > [dc setSecond: -[[NSTimeZone systemTimeZone] secondsFromGMTForDate: cd]];
    > cd = [[NSCalendar currentCalendar] dateByAddingComponents: dc toDate: cd options: 0];
    > NSLog( @"date 2: %@", [cd description] );
    >
    > If for example numdays is the number of days from 1/1/2001 to 5/4/2010, then the output is this:
    >
    > 2010-04-27 21:33:55.803 PedCard[95387:a0f] date 1: 2010-05-03 17:00:00 -0600
    > 2010-04-27 21:33:55.804 PedCard[95387:a0f] date 2: 2010-05-03 23:00:00 -0600
    >
    > And, BTW, my current time zone is MDT, so -0600 is not correct, it should be -0700. So what's going on? I would expect those dates to be:
    >
    > 2010-05-03 17:00:00 -0700
    > 2010-05-04 00:00:00 -0700
    >

    MDT is -0600 so this is correct - MST is -0700. (stupid "spring forward, fall back" clock resetting...)

    I find that dealing with dates and time zones it's best to consider NSDate to be just a scalar value, with a separate time zone (and optional calendar) being the "unit".  It's up to the formatter used to display that (scalar) date in the desired unit.

    Glenn Andreas                      <gandreas...>
    The most merciful thing in the world ... is the inability of the human mind to correlate all its contents - HPL
  • On Apr 28, 2010, at 8:11 AM, glenn andreas wrote:

    > MDT is -0600 so this is correct - MST is -0700. (stupid "spring forward, fall back" clock resetting...

    You're right about that (I was tired), but it's a read herring, the values are still wrong. Reference date is at midnight 1/1/2010 UTC, add integral days to it, the result should still be at midnight UTC.

    > I find that dealing with dates and time zones it's best to consider NSDate to be just a scalar value, with a separate time zone (and optional calendar) being the "unit".  It's up to the formatter used to display that (scalar) date in the desired unit.

    Well, this often works, but when you add days and it shifts the time back an hour, it doesn't work.

    --
    Scott Ribe
    <scott_ribe...>
    http://www.elevated-dev.com/
    (303) 722-0567 voice
  • On Apr 28, 2010, at 1:04 AM, Kubernan wrote:

    > Did you try to use NSDateComponents for building your reference date (2001/01/01) such as :

    No, reasonable idea though. But even better, I should have been logging the reference date after I created it. When I do that, I get:

    2000-12-31 17:00:00 -0700

    Which is correct, midnight at UTC. Then I add 3,412 days and I get:

    2010-05-05 17:00:00 -0600

    Which is off by an hour. So the problem seems to be with the addition, not with the creation of the reference date.

    --
    Scott Ribe
    <scott_ribe...>
    http://www.elevated-dev.com/
    (303) 722-0567 voice
  • On 28 Apr 2010, at 15:37:59, Scott Ribe wrote:

    > On Apr 28, 2010, at 1:04 AM, Kubernan wrote:
    >
    >> Did you try to use NSDateComponents for building your reference date (2001/01/01) such as :
    >
    > No, reasonable idea though. But even better, I should have been logging the reference date after I created it. When I do that, I get:
    >
    > 2000-12-31 17:00:00 -0700
    >
    > Which is correct, midnight at UTC. Then I add 3,412 days and I get:
    >
    > 2010-05-05 17:00:00 -0600
    >
    > Which is off by an hour. So the problem seems to be with the addition, not with the creation of the reference date.
    >

    Not read all the rest of the posts, but isn't that due to Daylight savings being different in December and May?

    Matt
  • Yes.  Yes it is.

    Dave (who also lives in MDT)

    On Apr 28, 2010, at 8:45 AM, Matt Gough wrote:

    > ... isn't that due to Daylight savings being different in December and May?
  • On 28-Apr-2010, at 10:37 PM, Scott Ribe wrote:

    > On Apr 28, 2010, at 1:04 AM, Kubernan wrote:
    >
    >> Did you try to use NSDateComponents for building your reference date (2001/01/01) such as :
    >
    > No, reasonable idea though. But even better, I should have been logging the reference date after I created it. When I do that, I get:
    >
    > 2000-12-31 17:00:00 -0700
    >
    > Which is correct, midnight at UTC. Then I add 3,412 days and I get:
    >
    > 2010-05-05 17:00:00 -0600
    >
    > Which is off by an hour. So the problem seems to be with the addition, not with the creation of the reference date.

    Are you sure this isn't the semantics of 'add a day'? There's two ways I can think that 'add n days' would work, one of them adds 24 * 60 * 60 seconds for each day you want to add, that's a simple way to do it.

    The other says ok the date I start at is 5pm in the timezone I'm set to (and NSCalendars have timezones) so let me give you 5pm (the same *time* ) in that TZ, on a date n days later. That it seems to me is what has actually happened here, you started at 5pm, you ended at 5pm 3412 days later. It just happens that one of those 5 PMs was in daylight savings time and the other wasn't, so you actually got 1 hour more/less than you wanted.

    Another way to put it, if it's 5pm here now and you say to your friend, I'll meet you here same time in a week, you'd expect to be there at 5pm a week later. If the clocks have changed between those two dates it doesn't matter, you'd show up at 5pm wall clock time.
  • OK, I get it. It's keeping the time of day the same in my local time zone, not UTC. To get the result I want, I need to:

    create the reference date
    subtract it's utc offset
    add days

    Instead of:

    create the reference date
    add days
    subtract the utc offset of the result.

    --
    Scott Ribe
    <scott_ribe...>
    http://www.elevated-dev.com/
    (303) 722-0567 voice
  • Scott Ribe wrote:

    > NSDate * cd = [[NSCalendar currentCalendar]
    dateByAddingComponents: dc toDate: [NSDate dateWithString:
    @"2001-01-01"] options: 0];

    I think the dateWithString: is wrong.  From the NSDate reference:

    "You must specify all fields of the format string, including the time
    zone offset, which must have a plus or minus sign prefix."

    Refactored code fragments:

    NSDate * refDate1 = [NSDate dateWithString: @"2001-01-01"];
    NSDate * refDate2 = [NSDate dateWithString: @"2001-01-01 00:00:00
    +0000"];

    NSLog( @"refDate1: %@", refDate1 );
    NSLog( @"refDate2: %@", refDate2 );

    ...
    NSDate * cd = [[NSCalendar currentCalendar] dateByAddingComponents: dc
      toDate: refDate1 options: 0];  // should be same as original

    ...
    NSDate * cd = [[NSCalendar currentCalendar] dateByAddingComponents: dc
      toDate: refDate2 options: 0];

      -- GG
  • A couple suggestions:

    If you always calculate using noon on any given date, rather than
    midnight, then DST transitions won't affect the year/month/day
    components.

    If you don't want to use noon, then NSTimeZone
    daylightSavingTimeOffsetForDate: should be taken into account.

      -- GG
previous month april 2010 next month
MTWTFSS
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30    
Go to today