4

Say I have a date and I want to do two things:

  1. Reset the hour to a specified one.
  2. Increment the day by a certain amount.

By reading the documentation, it looks like you need to use two distinct steps:

// Step 1: Get today at 9 AM.
NSDateComponents *todayComponents = [[NSCalendar currentCalendar] components:(NSCalendarUnitSecond | NSCalendarUnitMinute | NSCalendarUnitHour | NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear ) fromDate:[NSDate date]];
[todayComponents setHour:9];
NSDate *todayAt9AM = [[NSCalendar currentCalendar] dateFromComponents:todayComponents];

// Step 2: Add one day to that date.
NSDateComponents *oneDay = [NSDateComponents new];
[oneDay setDay:1];
NSDateComponents *tomorrowAt9AM = [[NSCalendar currentCalendar] dateByAddingComponents:oneDay toDate:todayAt9AM options:0];

However, it seems like there is a shortcut which involves only one step:

NSDateComponents *components = [[NSCalendar currentCalendar] components:(NSCalendarUnitSecond | NSCalendarUnitMinute | NSCalendarUnitHour | NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear ) fromDate:[NSDate date]];
components.hour = 9;
components.day += 1;
NSDate *tomorrowAt9AM = [[NSCalendar currentCalendar] dateFromComponents:components];

This seems to work even when the date is 1/31/2014, wrapping around to 2/1/2014 at 9AM correctly.

The reason this seems strange is because you are sending dateFromComponents the following values:

  • Year: 2014
  • Month: 1
  • Day: 32
  • Hour: 9
  • Minute: 34 (for example)
  • Seconds: 12 (for example)

Notice how 32 is out of range in the Gregorian calendar.

Is it dangerous to rely on this shortcut (e.g. if it only happens to be working by chance)? Or is there any documentation for dateFromComponents that says it can be used in this way?

Also, how does it know when I want wrapping to occur vs. when I want to explicitly override one of these values?

Say I had set the month to 3, would it leave the month at 3, or does it first process the month I specified (3), and then process the days which could potentially increment month to 4? Is this undefined behavior, or is there some documented set of rules it follows?

Senseful
  • 86,719
  • 67
  • 308
  • 465

1 Answers1

3

I don't have a real proof or documentation reference, but I have found some indicators that this "wrap around" in the date calculations actually works.

  • NSCalendar is toll-free bridged to its Core Foundation counterpart, CFCalendarRef.

  • From the source code CFCalendar.c one can see that all calendrical calculations are made using the ICU Calendar Classes.

  • The Calendar class has a method setLenient:

    /**
     * Specifies whether or not date/time interpretation is to be lenient. With lenient
     * interpretation, a date such as "February 942, 1996" will be treated as being
     * equivalent to the 941st day after February 1, 1996. With strict interpretation,
     * such dates will cause an error when computing time from the time field values
     * representing the dates.
     *
     * @param lenient  True specifies date/time interpretation to be lenient.
     *
     * @see            DateFormat#setLenient
     * @stable ICU 2.0
     */
    void setLenient(UBool lenient);
    
  • Finally, from the constructor in calendar.cpp one can see that the default "lenient" value is TRUE:

    Calendar::Calendar(UErrorCode& success)
    :   UObject(),
        fIsTimeSet(FALSE),
        fAreFieldsSet(FALSE),
        fAreAllFieldsSet(FALSE),
        fAreFieldsVirtuallySet(FALSE),
        fNextStamp((int32_t)kMinimumUserStamp),
        fTime(0),
        fLenient(TRUE),
        fZone(0)
    {
        clear();
        fZone = TimeZone::createDefault();
        setWeekCountData(Locale::getDefault(), NULL, success);
    }
    

Putting all this together, we get that setting

components.month = 1;
components.day = 32;

is treated "leniently" as the 31st day after January 1st, i.e. as February 1st.

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Does this lenient apply to other components (month, hour, min, sec) as well? For example, if I were to set the year to 2014, month to 13, and the day to 1 (2014-13-01), would it set the date to 2015-01-01? – Senseful Mar 03 '14 at 21:00
  • @Senseful: My experiments show that it does work for the other components as well, and that is how I understand the "lenient" property. Unfortunately, I do not have a "proof". – Martin R Mar 03 '14 at 21:06
  • Interesting! And does it look like it processes the components from biggest to smallest (year, month, day, hour, min, sec)? Which would explain how it knows when you want to wrap vs. explicitly set a value. – Senseful Mar 03 '14 at 21:12
  • @Senseful: Yes, it looks so. - One could probably answer it precisely by analysing the ICU calendar methods because the source code is available :-) – Martin R Mar 03 '14 at 21:22