objective-ccore-foundation

How would I add only business days to an NSDate?


I have an issue related to calculating business days in Objective-C.

I need to add X business days to a given NSDate.

For example, if I have a date: Friday 22-Oct-2010, and I add 2 business days, I should get: Tuesday 26-Oct-2010.

Thanks in advance.


Solution

  • There are two parts to this:

    I'm going to pull from two other posts to help me out.

    For weekends, I'm going to need to know a given date's day of the week. For that, this post comes in handy: How to check what day of the week it is (i.e. Tues, Fri?) and compare two NSDates?

    For holidays, @vikingosegundo has a pretty great suggestion on this post: List of all American holidays as NSDates

    First, let's deal with the weekends;

    I've wrapped up the suggestion in the post I cited above into this nice little helper function which tells us if a date is a weekday:

    BOOL isWeekday(NSDate * date)
    {
        int day = [[[NSCalendar currentCalendar] components:NSWeekdayCalendarUnit fromDate:date] weekday];
    
        const int kSunday = 1;
        const int kSaturday = 7;
    
        BOOL isWeekdayResult = day != kSunday && day != kSaturday;
    
        return isWeekdayResult;
    }
    

    We'll need a way to increment a date by a given number of days:

    NSDate * addDaysToDate(NSDate * date, int days)
    {
        NSDateComponents * components = [[NSDateComponents alloc] init];
        [components setDay:days];
    
        NSDate * result = [[NSCalendar currentCalendar] dateByAddingComponents:components toDate:date options:0];
    
        [components release];
    
        return result;
    }
    

    We need a way to skip over weekends:

    NSDate * ensureDateIsWeekday(NSDate * date)
    {
        while (!isWeekday(date))
        {
            // Add one day to the date:
            date = addDaysToDate(date, 1);
        }
    
        return date;
    }
    

    And we need a way to add an arbitrary number of days to a date:

    NSDate * addBusinessDaysToDate(NSDate * start, int daysToAdvance)
    {
        NSDate * end = start;
    
        for (int i = 0; i < daysToAdvance; i++)
        {
            // If the current date is a weekend, advance:
            end = ensureDateIsWeekday(end);
    
            // And move the date forward by one day:
            end = addDaysToDate(end, 1);
        }
    
        // Finally, make sure we didn't end on a weekend:
        end = ensureDateIsWeekday(end);
    
        return end;
    }
    

    Now lets tie that up and see what we have so far:

    int main() {
    
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
        NSDate * start = [NSDate date];
        int daysToAdvance = 10;
    
        NSDate * end = addBusinessDaysToDate(start, daysToAdvance);
    
        NSLog(@"Result: %@", [end descriptionWithCalendarFormat:@"%Y-%m-%d"
                                        timeZone:nil
                                          locale:nil]);
    
        [pool drain];
    
        return 0;
    }
    

    So, we've got weekends covered, now we need to pull in the holidays.

    Pulling in some RSS feed, or data from another source is definitely beyond the scope of my post... so, let's just assume you have some dates you know are holidays, or, according to your work calendar, are days off.

    Now, I'm going to do this with an NSArray... but, again, this leaves plenty of room for improvement - at minimum it should be sorted. Better yet, some sort of hash set for fast lookups of dates. But, this example should suffice to explain the concept. (Here we construct an array which indicates there are holidays two and three days from now)

    NSMutableArray * holidays = [[NSMutableArray alloc] init];
    [holidays addObject:addDaysToDate(start, 2)];
    [holidays addObject:addDaysToDate(start, 3)];
    

    And, the implementation for this will be very similar to the weekends. We'll make sure the day isn't a holiday. If it is, we'll advance to the next day. So, a collection of methods to help with that:

    BOOL isHoliday(NSDate * date, NSArray * holidays)
    {
        BOOL isHolidayResult = NO;
    
        const unsigned kUnits = NSYearCalendarUnit | NSMonthCalendarUnit |  NSDayCalendarUnit;
        NSDateComponents * components = [[NSCalendar currentCalendar] components:kUnits fromDate:date];
    
        for (int i = 0; i < [holidays count]; i++)
        {
            NSDate * holiday = [holidays objectAtIndex:i];
            NSDateComponents * holidayDateComponents = [[NSCalendar currentCalendar] components:kUnits fromDate:holiday];
    
            if ([components year] == [holidayDateComponents year]
                && [components month] == [holidayDateComponents month]
                && [components day] == [holidayDateComponents day])
                {
                    isHolidayResult = YES;
                    break;
                }
        }
    
        return isHolidayResult;
    }
    

    and:

    NSDate * ensureDateIsntHoliday(NSDate * date, NSArray * holidays)
    {
        while (isHoliday(date, holidays))
        {
            // Add one day to the date:
            date = addDaysToDate(date, 1);
        }
    
        return date;
    }
    

    And, finally, make some modifications to our addition function to take into account the holidays:

    NSDate * addBusinessDaysToDate(NSDate * start, int daysToAdvance, NSArray * holidays)
    {
        NSDate * end = start;
    
        for (int i = 0; i < daysToAdvance; i++)
        {
            // If the current date is a weekend, advance:
            end = ensureDateIsWeekday(end);
    
            // If the current date is a holiday, advance: 
            end = ensureDateIsntHoliday(end, holidays);
    
            // And move the date forward by one day:
            end = addDaysToDate(end, 1);
        }
    
        // Finally, make sure we didn't end on a weekend or a holiday:
        end = ensureDateIsWeekday(end);
        end = ensureDateIsntHoliday(end, holidays);
    
        return end;
    }
    

    Go ahead and try it:

    int main() {
    
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
        NSDate * start = [NSDate date];
        int daysToAdvance = 10;
    
        NSMutableArray * holidays = [[NSMutableArray alloc] init];
        [holidays addObject:addDaysToDate(start, 2)];
        [holidays addObject:addDaysToDate(start, 3)];
    
        NSDate * end = addBusinessDaysToDate(start, daysToAdvance, holidays);
    
        [holidays release];
    
        NSLog(@"Result: %@", [end descriptionWithCalendarFormat:@"%Y-%m-%d"
                                        timeZone:nil
                                          locale:nil]);
    
        [pool drain];
    
        return 0;
    }
    

    If you want the whole project, here ya go: http://snipt.org/xolnl