Using today as an example, how do I determine which date it was, 230 workdays ago?
I know how to do it iteratively with a while loop checking date and subtracting 1 if it's a workday, but I'm wondering if there is a better method.
Also, let's take a Sunday 1 PM as an example, and subtract 3 work days and 2 hours from that time. First, it doesn't make sense to subtract work-time from weekends. So it would have to move the time to 23:59:59 of Friday, and then subtract those 3 days and 2 hours.
If it's a Monday at 1:30 AM, and I'm subtracting 5 days and 3 work-hours from that time, then the result should be Friday 22:30 PM of the previous week.
Code to test Kevin's method:
NSCalendar *cal = [NSCalendar currentCalendar];
NSDateComponents *dc = [[NSDateComponents new] autorelease];
dc.month = 12;
dc.day = 19;
dc.year = 2011;
dc.hour = 1;
dc.minute = 0;
dc.second = 0;
NSDate *date = [cal dateFromComponents:dc];
NSLog(@"%@", [date descriptionWithCalendarFormat:nil timeZone:nil locale:nil]);
date = dateBySubtractingWorkOffset(date, 0, 2);
NSLog(@"%@", [date descriptionWithCalendarFormat:nil timeZone:nil locale:nil]);
Output log:
2011-12-02 16:33:46.878 otest[7124:707] 2011-12-19 01:00:00 -0500
2011-12-02 16:33:47.659 otest[7124:707] 2011-12-18 23:00:00 -0500
It should never be 12-18, since that's a Sunday.
Figure out how long from the last weekend your date is, subtract that amount from both your date and your offset. Now you can divide your offset by 5 to figure out how many full weeks are in your offset, and then multiply that by 7 and subtract this new value from your date. Take your previous offset (the one you divided by 5) and mod it by 5, to get the number of remaining days. If it's greater than 0, subtract that offset + 2 (for the weekend) from your date.
Note, this assumes every single weekday is a workday. Corporate holidays tend to make that assumption invalid. If you need to handle holidays, you're in for a much tougher problem.
Update: Here's an attempt to fix David's code to actually express the idea here:
NSDate *dateBySubtractingWorkOffset(NSDate *date, NSUInteger days, NSUInteger hours) {
const int secsInHour = 60*60;
const int secsInDay = 24*secsInHour;
NSTimeInterval offset = days*secsInDay + hours*secsInHour;
NSCalendar *cal = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
// figure out distance from last weekend
{
NSUInteger units = NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit|NSWeekdayCalendarUnit;
NSDateComponents *dc = [cal components:units fromDate:date];
if (dc.weekday == 1 || dc.weekday == 7) {
// we're in the weekend already. Let's just back up until friday
// and then we can start our calculations there
} else {
// figure out our offset from sunday 23:59:59
dc.day -= (dc.weekday - 1);
dc.weekday = 1;
dc.hour = 23;
dc.minute = 23;
dc.second = 23;
NSDate *sunday = [cal dateFromComponents:dc];
NSTimeInterval newOffset = [date timeIntervalSinceDate:sunday];
if (offset < newOffset) {
// our offset doesn't even go back to sunday, we don't need any calculations
return [date dateByAddingTimeInterval:-offset];
}
offset -= [date timeIntervalSinceDate:sunday];
// Now we can jump back to Friday with our new offset
}
// Calculate last friday at 23:59:59
dc.day -= (dc.weekday % 7 + 1);
dc.hour = 23;
dc.minute = 59;
dc.second = 59;
date = [cal dateFromComponents:dc];
}
// We're now set to Friday 23:59:59
// Lets figure out how many weeks we have
int secsInWorkWeek = 5*secsInDay;
NSInteger weeks = (NSInteger)trunc(offset / secsInWorkWeek);
offset -= weeks*secsInWorkWeek;
if (weeks > 0) {
// subtract that many weeks from the date
NSDateComponents *dc = [[NSDateComponents alloc] init];
dc.week = -weeks;
date = [cal dateByAddingComponents:dc toDate:date options:0];
[dc release];
}
// now we can just subtract our remaining offset from the date
return [date dateByAddingTimeInterval:-offset];
}
I haven't exhaustively test this, but it's based on some category methods I use regularly. To determine how many weekdays are between date1 and date2 (assumes date1 < date2), divide the return value of this function by 24*60*60 (the number of seconds in a day).
This splits the calculation into number of days before the first weekend, number of days after the last weekend and number of days in the intervening weeks. A weekend starts on Saturday at 00:00:00 hours and ends on Sunday at 23:59:59 hours. Typically you want to avoid assuming that a day has 24 hours in it, because there may be special cases associated with daylight savings time. So I recommend using NSCalendar to calculate time intervals when this is important. But that happens on weekends, so it is not significant for this case.
There are two methods here. The first returns the NSDate end date if you provide a start date and the number of working days (weekdays) you want to extend out to. (An earlier date is returned if the number of working days is negative.) The second returns the number of seconds that correspond to number of working days (including fractional days) between two given NSDate dates.
I tried to keep calculations within a timezone, but defaulted to the system timezone. (By the way, if you want to calculate with fractional days, change the weekdays
parameter to a float. Or you may want to calculate using a parameter in seconds. If so, then also change the calculation of totalInterval in the first method. You won't have to convert to seconds. All subsequent calculations in that method are done in seconds.)
- (NSDate*) calculateWeekDaysEndDateFrom:(NSDate*)_date1 and:(int)weekdays {
NSTimeInterval dayInterval = 24*60*60;
NSTimeInterval totalInterval = dayInterval * (float) weekdays;
NSTimeInterval secondsBeforeWeekend;
NSTimeInterval secondsAfterWeekend;
NSTimeInterval secondsInInterveningWeeks;
int numberOfWeeks;
NSDate *dateOfFirstSaturdayMorning;
NSDate *dateOfLastSundayNight;
NSDate *finalDate;
if (weekdays >0) {
dateOfFirstSaturdayMorning = [_date1 theFollowingWeekend];
secondsBeforeWeekend = [dateOfFirstSaturdayMorning timeIntervalSinceDate:_date1];
numberOfWeeks = (int)((totalInterval - secondsBeforeWeekend)/(5.0 * dayInterval));
secondsInInterveningWeeks = 5 * (float)(numberOfWeeks * dayInterval);
secondsAfterWeekend = totalInterval - secondsBeforeWeekend - secondsInInterveningWeeks;
dateOfLastSundayNight = [[dateOfFirstSaturdayMorning dateByAddingDays:7*numberOfWeeks+2] dateByAddingTimeInterval:-1]; // move from saturday morning to monday morning, then back off 1 second
finalDate = [dateOfLastSundayNight dateByAddingTimeInterval:secondsAfterWeekend];
}
else {
dateOfLastSundayNight = [_date1 thePreviousWeekend];
secondsAfterWeekend = [date1 timeIntervalSinceDate:dateOfLastSundayNight];
numberOfWeeks = (int)((-totalInterval - secondsAfterWeekend)/(5.0 * dayInterval));
secondsInInterveningWeeks = 5 * (float)(numberOfWeeks * dayInterval);
dateOfFirstSaturdayMorning = [[dateOfLastSundayNight dateByAddingDays:-(7*numberOfWeeks+2)] dateByAddingTimeInterval:+1];
secondsBeforeWeekend = -totalInterval - secondsInInterveningWeeks - secondsAfterWeekend;
finalDate = [dateOfFirstSaturdayMorning dateByAddingTimeInterval:-secondsBeforeWeekend];
}
NSLog(@"dateOfFirstSaturdayMorning = %@", [dateOfFirstSaturdayMorning descriptionWithLocale:[NSLocale currentLocale]]);
NSLog(@"dateOfLastSundayNight = %@",[dateOfLastSundayNight descriptionWithLocale:[NSLocale currentLocale]]);
NSLog(@"date 1 = %@", date1);
NSLog (@"daysBeforeWeekend = %.2f", secondsBeforeWeekend/((float)dayInterval));
NSLog (@"daysBetweenWeekends = %.2f", secondsInInterveningWeeks/((float)(dayInterval)));
NSLog (@"daysAfterWeekend = %.2f", secondsAfterWeekend/((float)dayInterval));
NSLog (@"numberOfWeekdays = %.2f", (secondsBeforeWeekend + secondsInInterveningWeeks + secondsAfterWeekend)/((float)dayInterval));
NSLog(@"endDateFromWeekdays = %@", [finalDate descriptionWithLocale:[NSLocale currentLocale]]);
return finalDate;
}
- (NSTimeInterval) calculateWeekdaysFrom:(NSDate*)_date1 and:(NSDate*)_date2 {
if (_date1 && _date2) {
NSTimeInterval secondsBeforeWeekend;
NSTimeInterval secondsAfterWeekend;
NSDate *dateOfFirstSaturdayMorning;
NSDate *dateOfLastSundayNight;
NSTimeInterval dayInterval = 24*60*60; // This isn't always true, e.g., if daylight savings intervenes. (But that happens on the weekend in most places.)
// see if they are in the same week
if (([_date1 ordinality] < [_date2 ordinality]) && [_date2 timeIntervalSinceDate:_date1] <= 5*dayInterval) {
return [_date2 timeIntervalSinceDate:_date1];
}
// time interval before a first weekend
if ([_date1 ordinality] == 1 || [_date1 ordinality] == 7) {
secondsBeforeWeekend = 0;
dateOfFirstSaturdayMorning = _date1; // This is just a convenience. It's not true. But, later, rounding takes place to deal with it.
}
else {
dateOfFirstSaturdayMorning = [_date1 theFollowingWeekend];
secondsBeforeWeekend = [dateOfFirstSaturdayMorning timeIntervalSinceDate:_date1];
}
int ordDate2 = [_date2 ordinality];
int ordFirstSaturday = [dateOfFirstSaturdayMorning ordinality];
// time interval after a last weekend
if ([_date2 ordinality] == 1 || [_date2 ordinality] == 7) {
secondsAfterWeekend = 0;
dateOfLastSundayNight = _date2; // Again, this is just a convenience. It's not true.
}
else {
dateOfLastSundayNight = [_date2 thePreviousWeekend];
secondsAfterWeekend = [_date2 timeIntervalSinceDate:dateOfLastSundayNight];
}
NSTimeInterval intervalBetweenWeekends = [dateOfLastSundayNight timeIntervalSinceDate:dateOfFirstSaturdayMorning];
int numberOfWeeks = (int) (intervalBetweenWeekends/(7*dayInterval));
int secondsInInterveningWeeks = (float) (5*dayInterval*numberOfWeeks);
NSLog(@"date 1 = %@", [_date1 descriptionWithLocale:[NSLocale currentLocale]]);
NSLog(@"date 2 = %@", [_date2 descriptionWithLocale:[NSLocale currentLocale]]);
NSLog(@"dateOfFirstSaturdayMorning = %@", [dateOfFirstSaturdayMorning descriptionWithLocale:[NSLocale currentLocale]]);
NSLog(@"dateOfLastSundayNight = %@",[dateOfLastSundayNight descriptionWithLocale:[NSLocale currentLocale]]);
NSLog (@"daysBeforeWeekend = %.2f", secondsBeforeWeekend/((float)dayInterval));
NSLog (@"daysBetweenWeekends = %.2f", secondsInInterveningWeeks/((float)(dayInterval)));
NSLog (@"daysAfterWeekend = %.2f", secondsAfterWeekend/((float)dayInterval));
NSLog (@"numberOfWeekdays = %.2f", (secondsBeforeWeekend + secondsInInterveningWeeks + secondsAfterWeekend)/((float)dayInterval));
return secondsBeforeWeekend + secondsInInterveningWeeks + secondsAfterWeekend;
}
else
return 0;
}
The files for category methods on NSDate are NSDate+help.h
@interface NSDate (help)
+ (NSDate *) LSExtendedDateWithNaturalLanguageString:(NSString *)dateString WithFormatter:(NSDateFormatter*)dateFormatter;
- (NSUInteger)ordinality;
- (NSDate*) theFollowingWeekend;
- (NSDate *) thePreviousWeekend;
- (NSDate *) dateByAddingDays:(NSInteger) numberOfDays;
- (NSDate *) dateByMovingToBeginningOfDayInTimeZone:(NSTimeZone*)tz;
- (NSDate *) dateByMovingToEndOfDayInTimeZone:(NSTimeZone*)tz;
@end
and NSDate+help.m
#import "NSDate+help.h"
@implementation NSDate (help)
// thrown in for testing
+ (NSDate *) LSExtendedDateWithNaturalLanguageString:(NSString *)dateString WithFormatter:(NSDateFormatter*)dateFormatter{
[dateFormatter setDateFormat:@"yyyy-MM-dd HHmm"];
[dateFormatter setLocale:[NSLocale currentLocale]];
//NSDate *formattedDate = [dateFormatter dateFromString:@"2008-12-3T22-11-30-123"];
return [dateFormatter dateFromString:dateString];
}
- (NSUInteger)ordinality {
NSCalendar *calendar = [NSCalendar currentCalendar];
[calendar setTimeZone:[NSTimeZone systemTimeZone]];
return [calendar ordinalityOfUnit:NSDayCalendarUnit inUnit:NSWeekCalendarUnit forDate:self];
}
- (NSDate*) theFollowingWeekend {
NSUInteger myOrdinality = [self ordinality];
NSDate *dateOfFollowingWeekend = [self dateByAddingDays:(7-myOrdinality)%7];
return [dateOfFollowingWeekend dateByMovingToBeginningOfDayInTimeZone:(NSTimeZone*)nil];
}
- (NSDate *) thePreviousWeekend {
NSUInteger myOrdinality = [self ordinality];
NSDate *dateOfPreviousWeekend = [self dateByAddingDays:(1-myOrdinality)];
return [dateOfPreviousWeekend dateByMovingToEndOfDayInTimeZone:(NSTimeZone*)nil];
}
- (NSDate *) dateByAddingDays:(NSInteger) numberOfDays {
NSDateComponents *dayComponent = [[NSDateComponents alloc] init];
dayComponent.day = numberOfDays;
NSCalendar *theCalendar = [NSCalendar currentCalendar];
return [theCalendar dateByAddingComponents:dayComponent toDate:self options:0];
}
- (NSDate *) dateByMovingToBeginningOfDayInTimeZone:(NSTimeZone*)tz {
NSTimeZone *timezone;
if (tz)
timezone = tz;
else
timezone = [NSTimeZone systemTimeZone];
unsigned int flags = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
NSDateComponents* parts = [[NSCalendar currentCalendar] components:flags fromDate:self];
[parts setHour:0];
[parts setMinute:0];
[parts setSecond:0];
NSCalendar *calendar = [NSCalendar currentCalendar];
[calendar setTimeZone:timezone];
return [calendar dateFromComponents:parts];
}
- (NSDate *)dateByMovingToEndOfDayInTimeZone:(NSTimeZone*)tz {
NSTimeZone *timezone;
if (tz)
timezone = tz;
else
timezone = [NSTimeZone systemTimeZone];
unsigned int flags = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
NSDateComponents* parts = [[NSCalendar currentCalendar] components:flags fromDate:self];
[parts setHour:23];
[parts setMinute:59];
[parts setSecond:59];
NSCalendar *calendar = [NSCalendar currentCalendar];
[calendar setTimeZone:timezone];
return [calendar dateFromComponents:parts];
}
@end
The category method ordinality
returns the number for the day of the week of the receiver. Sunday = 1, Saturday = 7. This is used to find out how many days there are before the end of the first week and how many days there are after the beginning the last week. (Calculations are actually carried out in seconds.)
The category methods theFollowingWeekend
and thePreviousWeekend
return the NSDate at midnight on the Saturday morning that follows the receiver date and the NSDate one second before midnight on the Sunday that follows the receiver date. These methods assume you have already validated that the receiver date is not on the weekend. I handled that in the main methods. Look for the checks of ordinality == 1 or 7.
dateByMovingToBeginningOfDayInTimeZone:
and dateByMovingToEndOfDayInTimeZone:
set the hours, minutes, and seconds of the receiver date to 00:00:00 and 23:59:59 respectively. This is for delimiting weekends, which run from midnight Saturday morning to midnight Sunday night in the timezone.
Hope this helps. This was an exercise for me to become more familiar with the time and date functionality.
I'll credit Keith Lazuka and his calendar component for iPhone for the germination of this code.
Here's a screen shot of a test program user interface that uses these functions:
Here is your example, run through the first method. The items of interest are highlighted. . For this, I made the simple modification to accept fractional days (which i mentioned above, but did not include in the code shown above)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With