So your definition of business days seems to be Mondays through Fridays. Let's call these “workdays”, as opposed to “weekends” (which are Saturday and Sunday).
It is not obvious how to compute the number of workdays from a startDate
to an endDate
, so let's start with a simpler computation: the number of weeks from startDate
to endDate
. By “week” I mean a seven day period starting on a Sunday.
If startDate
and endDate
fall in the same week, we'll count that week once. If they fall in different weeks, we'll count the week containing startDate
, the week containing endDate
, and any weeks in between. So this is an “inclusive” count of weeks.
Given this number of weeks, we can obviously multiply by 5 to get the number of workdays in the weeks. But this isn't necessarily the number of workdays from startDate
to endDate
. If startDate
is a Sunday or a Monday, and endDate
is a Saturday or a Sunday, then it's correct, but otherwise it counts some days that should be excluded.
How many days should be excluded? First, consider startDate
. We need to exclude the number of workdays that precede startDate
in the week (0 for Sunday and Monday, 1 for Tuesday, 2 for Wednesday, 3 for Thursday, 4 for Friday, and 5 for Saturday). Then consider endDate
. We need to exclude the number of workdays that follow endDate
in the week, and we need to exclude endDate
itself if it is a workday (5 for Sunday and Monday, 4 for Tuesday, 3 for Wednesday, 2 for Thursday, 1 for Friday, and 0 for Saturday).
Given all that, the formula for the number of workdays from startDate
to endDate
is
5 * inclusive count of weeks from startDate to endDate
- count of workdays preceding startDate
- count of workdays following and including endDate
This will, for example, give 1 if startDate
is a Monday and endDate
is the Tuesday of the same week. If you would rather get 2 in that case (in other words, you want to count endDate
as one of the workdays if it's not a weekend), then the formula is
5 * inclusive count of weeks from startDate to endDate
- count of workdays preceding startDate
- count of workdays following (but not including) endDate
Now let's write it in Objective-C using Cocoa's excellent calendar computation support. We'll implement this category on NSCalendar
:
@interface NSCalendar (Rob_Workdays)
/** I return the number of non-weekend days from startDate to endDate,
including the day containing `startDate` and excluding the day
containing `endDate`. */
- (NSInteger)Rob_countOfWorkdaysFromDate:(NSDate *)startDate
toDate:(NSDate *)endDate;
/** I return the number of non-weekend days from startDate to endDate,
including the day containing `startDate` and the day containing
`endDate`. */
- (NSInteger)Rob_countOfWorkdaysFromDate:(NSDate *)startDate
toAndIncludingDate:(NSDate *)endDate;
@end
To implement those methods, let's translate the formulae directly into code:
@implementation NSCalendar (Rob_Workdays)
- (NSInteger)Rob_countOfWorkdaysFromDate:(NSDate *)startDate toDate:(NSDate *)endDate {
return 5 * [self Rob_inclusiveCountOfWeeksFromDate:startDate toDate:endDate]
- [self Rob_countOfWorkdaysPrecedingDate:startDate]
- [self Rob_countOfWorkdaysFollowingAndIncludingDate:endDate];
}
- (NSInteger)Rob_countOfWorkdaysFromDate:(NSDate *)startDate toAndIncludingDate:(NSDate *)endDate {
return 5 * [self Rob_inclusiveCountOfWeeksFromDate:startDate toDate:endDate]
- [self Rob_countOfWorkdaysPrecedingDate:startDate]
- [self Rob_countOfWorkdaysFollowingDate:endDate];
}
Of course we need to implement the helper methods, starting with the count of weeks. To count weeks, we roll startDate
backward until it's a Sunday, and we roll endDate
forward until it's a Sunday. Then we compute the number of days from startDate
to endDate
and divide by 7:
/** I return the number of weeks from `startDate` to `endDate`, including
the weeks containing each date (but only counting the week once if the
same week includes both dates). */
- (NSInteger)Rob_inclusiveCountOfWeeksFromDate:(NSDate *)startDate
toDate:(NSDate *)endDate
{
startDate = [self Rob_sundayOnOrBeforeDate:startDate];
endDate = [self Rob_sundayAfterDate:endDate];
NSDateComponents *components = [self components:NSCalendarUnitDay
fromDate:startDate toDate:endDate options:0];
return components.day / 7;
}
Here's how we roll back to a Sunday:
- (NSDate *)Rob_sundayOnOrBeforeDate:(NSDate *)date {
return [self Rob_dateByAddingDays:1 - [self Rob_weekdayOfDate:date]
toDate:date];
}
And here's how we roll forward to a Sunday:
- (NSDate *)Rob_sundayAfterDate:(NSDate *)date {
return [self Rob_dateByAddingDays:8 - [self Rob_weekdayOfDate:date]
toDate:date];
}
Here's how we compute the weekday (from 1=Sunday to 7=Saturday) of a date:
- (NSInteger)Rob_weekdayOfDate:(NSDate *)date {
return [self components:NSCalendarUnitWeekday fromDate:date].weekday;
}
Here's how we add some number of days to a date:
- (NSDate *)Rob_dateByAddingDays:(NSInteger)days toDate:(NSDate *)date {
if (days != 0) {
NSDateComponents *components = [[NSDateComponents alloc] init];
components.day = days;
date = [self dateByAddingComponents:components toDate:date
options:0];
}
return date;
}
Note that if days
is negative, that method will roll the date back in time.
To compute the number of workdays preceding a date, we can either use brute force (since there are only seven days of the week to consider), which is easy to understand, or we can use a formula, which is smaller but harder to understand:
- (NSInteger)Rob_countOfWorkdaysPrecedingDate:(NSDate *)date {
switch ([self Rob_weekdayOfDate:date]) {
case 1: return 0;
case 2: return 0;
case 3: return 1;
case 4: return 2;
case 5: return 3;
case 6: return 4;
case 7: return 5;
default: abort();
}
// equivalently: return MIN(MAX(0, [self Rob_weekdayOfDate:date] - 2), 5);
}
Computing the number of workdays following a date is similar, but we need two versions, depending on whether we're including or excluding the given date:
- (NSInteger)Rob_countOfWorkdaysFollowingAndIncludingDate:(NSDate *)date {
switch ([self Rob_weekdayOfDate:date]) {
case 1: return 5;
case 2: return 5;
case 3: return 4;
case 4: return 3;
case 5: return 2;
case 6: return 1;
case 7: return 0;
default: abort();
}
// equivalently: return MIN(7 - [self Rob_weekdayOfDate:date], 5);
}
- (NSInteger)Rob_countOfWorkdaysFollowingDate:(NSDate *)date {
switch ([self Rob_weekdayOfDate:date]) {
case 1: return 5;
case 2: return 4;
case 3: return 3;
case 4: return 2;
case 5: return 1;
case 6: return 0;
case 7: return 0;
default: abort();
}
// equivalently: return MAX(6 - [self Rob_weekdayOfDate:date], 0);
}
@end