我有一个与 Objective-C 中计算工作日相关的问题。
我需要将
X
个工作日添加到给定的 NSDate
。
例如,如果我有一个日期:2010 年 10 月 22 日星期五,并且添加 2 个工作日,我应该得到:2010 年 10 月 26 日星期二。
提前致谢。
这有两个部分:
我将从另外两个帖子中提取内容来帮助我。
对于周末,我需要知道给定日期是一周中的哪一天。为此,这篇文章派上用场: 如何检查今天是星期几(即星期二、星期五?)并比较两个 NSDate?
对于假期,@vikingosegundo 在这篇文章中有一个非常好的建议: 所有美国假期的 NSDate 列表
首先,我们来处理一下周末;
我已将上面引用的帖子中的建议纳入这个漂亮的小帮助函数中,该函数告诉我们日期是否是工作日:
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;
}
我们需要一种方法来将日期增加给定的天数:
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;
}
我们需要一种方法来跳过周末:
NSDate * ensureDateIsWeekday(NSDate * date)
{
while (!isWeekday(date))
{
// Add one day to the date:
date = addDaysToDate(date, 1);
}
return 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;
}
现在让我们把它结合起来,看看我们到目前为止有什么:
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;
}
所以,我们已经度过了周末,现在我们需要考虑假期。
提取一些 RSS 提要或来自其他来源的数据绝对超出了我的帖子的范围...因此,我们假设您知道某些日期是假期,或者根据您的工作日历,是休息日。
现在,我将使用 NSArray 来完成此操作...但是,这又留下了很大的改进空间 - 至少应该对其进行排序。更好的是,某种用于快速查找日期的哈希集。但是,这个例子应该足以解释这个概念。 (这里我们构造一个数组,表示从现在起两三天后有假期)
NSMutableArray * holidays = [[NSMutableArray alloc] init];
[holidays addObject:addDaysToDate(start, 2)];
[holidays addObject:addDaysToDate(start, 3)];
并且,其实施将与周末非常相似。我们将确保这一天不是假期。如果是的话,我们将提前到第二天。因此,有一系列方法可以帮助解决这个问题:
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;
}
并且:
NSDate * ensureDateIsntHoliday(NSDate * date, NSArray * holidays)
{
while (isHoliday(date, holidays))
{
// Add one day to the date:
date = addDaysToDate(date, 1);
}
return date;
}
最后,对我们的添加函数进行一些修改以考虑假期:
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;
}
继续尝试一下:
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;
}
如果您想要整个项目,请访问:http://snipt.org/xolnl
NSDate 或 NSCalendar 中没有内置任何内容可以为您计算工作日。工作日在某种程度上取决于相关业务。在美国,“工作日”通常是指非节假日的工作日,但每个公司都会确定要遵守哪些节假日以及何时遵守节假日。例如,一些企业将小假期推迟到一年中的最后一周,以便员工可以在圣诞节和元旦之间休息,而无需休假。
因此,您需要准确确定工作日的含义。然后,编写一个小方法来通过添加一些工作日来计算未来日期应该很简单。然后使用类别向 NSDate 添加类似
-dateByAddingBusinessDays:
的方法。
这个答案来晚了,但是……我想我可以改进上面的答案,通过在一个很好的循环中直接使用您的日期的 NSDateComponents 来确定工作日。
#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
将其转回 NSDate,就完成了。 假期涉及的更多......但如果仅使用联邦假期和其他一些假期,实际上可以计算出来。 因为它们总是类似于“一月的第三个星期一”
这就是 findHolidaysStart:startComp end: 开始的样子,剩下的你可以想象。
// 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];
我采纳了@steve的答案,并添加了一种方法来计算美国所有联邦假日的天数,并将其全部放入一个类别中。 我已经测试过了,效果很好。 看看吧。
#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
我只需要在周末跳来跳去,所以我真的不想解析 ObjC 答案,只获取相关的实现,并将它们转换为 Swift,所以我在 Swift 5 中编写了自己的代码:
extension Date {
func addingBusinessDays(_ dayCount: Int) -> Date {
let calendar = Calendar.current
// Monday through Friday.
let weekdaySet = IndexSet([2, 3, 4, 5, 6])
var date = self
var daysToSkip = dayCount
while daysToSkip > 0 {
date = calendar.date(byAdding: .day, value: 1, to: date)!
if weekdaySet.contains(calendar.component(.weekday, from: date)) {
daysToSkip -= 1
}
}
return date
}
}
由于我并没有真正尝试理解前面的答案,所以这个粗略的实现可能会更慢,它只是迭代,直到达到开发人员期望的工作日跳跃数量。
我确信日历的查找下周末的功能可以用来优化它,但对于较小的时间跳过,延迟可以忽略不计。 🤷