How would I add only business days to an NSDate?

心不动则不痛 提交于 2019-12-03 11:03:51

问题


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.


回答1:


There are two parts to this:

  • Weekends
  • Holidays

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;
}
  • Note; There is an obvious optimization I skipped - you could easily add more than one day at a time to the current date - but the point of my post is to show you how to do this yourself - and not necessarily to come up with the best possible solution.

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




回答2:


There's nothing built into NSDate or NSCalendar that counts business days for you. Business days depend to some degree on the business in question. In the US, "business day" generally means weekdays that aren't holidays, but each company determines which holidays to observe and when. For example, some businesses move observance of minor holidays to the last week of the year so that employees can be off between Christmas and New Year's Day without taking vacation.

So, you'll need to decide exactly what you mean by business day. It should then be simple enough write a little method to calculate a future date by adding some number of business days. Then use a category to add a method like -dateByAddingBusinessDays: to NSDate.




回答3:


this answer is late to the party but…. I thought i might improve on above answers to determine business days by working with NSDateComponents directly of your date in a nice loop.

#define CURRENTC [NSCalendar currentCalendar]
#define CURRENTD [NSDate date]

NSInteger theWeekday;

    NSDateComponents* temporalComponents = [[NSDateComponents alloc] init];

[temporalComponents setCalendar:CURRENTC];
[temporalComponents setDay:   13];
[temporalComponents setMonth: 2];
[temporalComponents setYear: theYear];

// CURRENTC =the current calendar which determines things like how 
// many days in week for local,  also the critical “what is a weekend”
// you can also convert a date directly to components.  but the critical thing is
// to get the CURRENTC in, either way.

   case 3:{    // the case of finding business days
        NSDateComponents* startComp = [temporalComponents copy];  // start date components

        for (int i = 1; i <= offset; i++)  //offset is the number of busi days you want.
        {
            do {
                [temporalComponents setDay:   [temporalComponents day] + 1];
                NSDate* tempDate = [CURRENTC dateFromComponents:temporalComponents];
                theWeekday = [[CURRENTC components:NSWeekdayCalendarUnit fromDate:tempDate] weekday];
            } while ((theWeekday == 1) || (theWeekday == 7));
        }
        [self findHolidaysStart:startComp end:temporalComponents];  // much more involved routine.

        [startComp release];
        break;
     }

// use startComp and temporalcomponents before releasing

// temporalComponents now contain an offset of the real number of days 
// needed to offset for busi days.  startComp is just your starting date….(in components)
// theWeekday is an integer between 1 for sunday, and 7 for saturday,  (also determined
// by CURRENTC

turning this back into NSDate, and you are done. Holidays are much more involved.. but can actually be calculated if just using federal holidays and a few others. because they are always something like “3rd monday of January”

here is what the findHolidaysStart:startComp end: starts out like, you can imagine the rest.

// imported

    [holidayArray addObject:[CURRENTC dateFromComponents:startComp]];
    [holidayArray addObject:[CURRENTC dateFromComponents:endComp]];


// hardcoded

   dateComponents = [[NSDateComponents alloc] init];
    [dateComponents setCalendar:CURRENTC];
    [dateComponents setDay:   1];
    [dateComponents setMonth: 1];
    [dateComponents setYear: theYear];

    theWeekday = [[CURRENTC components:NSWeekdayCalendarUnit fromDate:[CURRENTC dateFromComponents:dateComponents]] weekday];

    if (theWeekday == 1) [dateComponents setDay:2];
    if (theWeekday == 7) {[dateComponents setDay:31]; [dateComponents setYear: theYear-1];}

    [holidayArray addObject:[CURRENTC dateFromComponents:dateComponents]];
    [dateComponents release];



回答4:


I took @steve's answer and added a method to calculate the days of all the federal holidays in USA and put it all in a Category. I've tested it and it works nicely. Check it out.

#import "NSDate+BussinessDay.h"

@implementation NSDate (BussinessDay)

-(NSDate *)addBusinessDays:(int)daysToAdvance{
    NSDate * end = self;
    NSArray *holidays = [self getUSHolidyas];
    for (int i = 0; i < daysToAdvance; i++)
    {
        // Move the date forward by one day:
        end = [self addDays:1 toDate:end];

        // If the current date is a weekday, advance:
        end = [self ensureDateIsWeekday:end];

        // If the current date is a holiday, advance:
        end = [self ensureDateIsntHoliday:end forHolidays:holidays];
    }

    return end;
}

#pragma mark - Bussiness Days Calculations

-(BOOL)isWeekday:(NSDate *) date{
    int day = (int)[[[NSCalendar currentCalendar] components:NSWeekdayCalendarUnit fromDate:date] weekday];

    const int kSunday = 1;
    const int kSaturday = 7;

    BOOL isWeekdayResult = day != kSunday && day != kSaturday;
    return isWeekdayResult;
}

-(NSDate *)addDays:(int)days toDate:(NSDate *)date{
    NSDateComponents * components = [[NSDateComponents alloc] init];
    [components setDay:days];

    NSDate * result = [[NSCalendar currentCalendar] dateByAddingComponents:components toDate:date options:0];
    return result;
}

-(NSDate *)ensureDateIsWeekday:(NSDate *)date{
    while (![self isWeekday:date])
    {
        // Add one day to the date:
        date = [self addDays:1 toDate:date];
    }

    return date;
}

-(BOOL)isHoliday:(NSDate *)date forHolidays:(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;
}

-(NSDate *)ensureDateIsntHoliday:(NSDate *)date forHolidays:(NSArray *)holidays{
    while ([self isHoliday:date forHolidays:holidays])
    {
        // Add one day to the date:
        date = [self addDays:1 toDate:date];
    }

    return date;
}

-(NSArray *)getUSHolidyas{
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    formatter.dateFormat = @"yyyy";

    NSString *year = [formatter stringFromDate:[NSDate date]];
    NSString *nextYear = [formatter stringFromDate:[NSDate dateWithTimeIntervalSinceNow:(60*60*24*365)]];
    formatter.dateFormat = @"M/d/yyyy";

    //Constant Holidays
    NSDate *newYearsDay = [formatter dateFromString:[NSString stringWithFormat:@"1/1/%@",nextYear]]; //Use next year for the case where we are adding days near end of december.
    NSDate *indDay = [formatter dateFromString:[NSString stringWithFormat:@"7/4/%@",year]];
    NSDate *vetDay = [formatter dateFromString:[NSString stringWithFormat:@"11/11/%@",year]];
    NSDate *xmasDay = [formatter dateFromString:[NSString stringWithFormat:@"12/25/%@",year]];


    //Variable Holidays
    NSInteger currentYearInt = [[[NSCalendar currentCalendar]
                                 components:NSYearCalendarUnit fromDate:[NSDate date]] year];

    NSDate *mlkDay = [self getTheNth:3 occurrenceOfDay:2 inMonth:1 forYear:currentYearInt];
    NSDate *presDay = [self getTheNth:3 occurrenceOfDay:2 inMonth:2 forYear:currentYearInt];
    NSDate *memDay = [self getTheNth:5 occurrenceOfDay:2 inMonth:5 forYear:currentYearInt]; // Let's see if there are 5 Mondays in May
    NSInteger month = [[[NSCalendar currentCalendar] components:NSYearCalendarUnit fromDate:memDay] month];
    if (month > 5) { //Check that we are still in May
        memDay = [self getTheNth:4 occurrenceOfDay:2 inMonth:5 forYear:currentYearInt];
    }
    NSDate *labDay = [self getTheNth:1 occurrenceOfDay:2 inMonth:9 forYear:currentYearInt];
    NSDate *colDay = [self getTheNth:2 occurrenceOfDay:2 inMonth:10 forYear:currentYearInt];
    NSDate *thanksDay = [self getTheNth:4 occurrenceOfDay:5 inMonth:11 forYear:currentYearInt];

    return @[newYearsDay,mlkDay,presDay,memDay,indDay,labDay,colDay,vetDay,thanksDay,xmasDay];
}

-(NSDate *)getTheNth:(NSInteger)n occurrenceOfDay:(NSInteger)day inMonth:(NSInteger)month forYear:(NSInteger)year{

    NSDateComponents *dateComponents = [[NSDateComponents alloc] init];

    dateComponents.year = year;
    dateComponents.month = month;
    dateComponents.weekday = day; // sunday is 1, monday is 2, ...
    dateComponents.weekdayOrdinal = n; // this means, the first of whatever weekday you specified
    return [[NSCalendar currentCalendar] dateFromComponents:dateComponents];
}

@end


来源:https://stackoverflow.com/questions/7871435/how-would-i-add-only-business-days-to-an-nsdate

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!