Programming Without Walls
Software Engineering with Microsoft .net

Calculating Work Days

April 24, 2008 by Bob Barth

A couple of weeks ago a client of mine asked for a feature in which the software could display the number of days a piece of work was overdue. That's not a problem. Naturally, however, they wanted it to be work days. I didn't want to explain how computers are unbelievably bad at messing with dates so I just said "sure."

The other day I came to the part in the project where I had to implement this feature. Figuring someone, somewhere had figured out how to do this (and published it in a blog or something), I perused Google. Unfortunately, the solutions I found either involved using the Microsoft.VisualBasic assembly and the DateDiff function (which don't work*) or writing a row to a database table for every day in the year, with various shenanigans to determine whether the day is a work day or not, running the gamut from computed columns (which I actually liked) to indexing the entire year by number, skipping non-work days.

I really did not want to create a solution in which my client would need to tediously create a new calendar for every day of the year, every year. I'm sure I could have written a nice UI and everything but I didn't want to waste the time or money. Instead, I figured I could write an algorithm that figures out weekends mathematically. The result follows. It took me about three hours before I had something that worked and I don't guarantee this works for every two valid date combinations, but it passed the 27 tests I could think of.

   1: /// <summary>
   2: /// Calculates the whole number of work days between two arbitrary dates.
   3: /// </summary>
   4: /// <param name="startDate">The first date of the range to check.</param>
   5: /// <param name="endDate">The second date of the range to check.</param>
   6: /// <param name="nonWorkDays">An array of DateTime values indicating non-work days. Note: This
   7: /// array cannot contain days that occur on weekends.</param>
   8: /// <returns>The count of whole work days that occur between startDate and endDate, inclusive.</returns>
   9: public static int CalculateWorkdaysCount(
  10:             DateTime startDate,
  11:             DateTime endDate,
  12:             DateTime[] nonWorkDays)
  13: {
  14:     // Zero the time
  15:     DateTime sdt = startDate.Subtract(startDate.TimeOfDay);
  16:     DateTime edt = endDate.Subtract(endDate.TimeOfDay);
  17:  
  18:     if (sdt > edt)
  19:         return 0;
  20:  
  21:     // Get an initial workdays count
  22:     int workDays = (int)edt.Subtract(sdt).TotalDays + 1;
  23:     int weeks = workDays / 7;
  24:  
  25:     // Subtract the weekends indicated by the number of whole weeks the dates span
  26:     workDays -= weeks * 2;
  27:     // Remove the whole weeks from the span and figure out what is left
  28:     edt = edt.AddDays(-weeks * 7);
  29:  
  30:     // Subtracting the whole weeks leaves us with a partial week. If the date span is a
  31:     // a multiple of seven, edt will be == sdt and need not be checked. At this point,
  32:     // anything over a week looks the same as if the date span were less than a week, originally.
  33:     if (edt > sdt)
  34:     {
  35:         // If the start and end dates span a weekend, decrement the work day count by two
  36:         if (sdt.DayOfWeek > edt.DayOfWeek && edt.DayOfWeek < DayOfWeek.Saturday && edt.DayOfWeek > DayOfWeek.Sunday)
  37:             workDays -= 2;
  38:         else
  39:         {
  40:             // If the start date is a Saturday or Sunday, decrement the work day count
  41:             if (sdt.DayOfWeek == DayOfWeek.Saturday || sdt.DayOfWeek == DayOfWeek.Sunday)
  42:                 workDays--;
  43:  
  44:             // If the end date is a Saturdayor Sunday, decrement the work day count
  45:             if (edt.DayOfWeek == DayOfWeek.Saturday || edt.DayOfWeek == DayOfWeek.Sunday)
  46:                 workDays--;
  47:         }
  48:     }
  49:  
  50:     // Remove the non-work days from the count if they are between the (original) startDate and endDate.
  51:     // Note: the times of the non-work days are zeroed.
  52:     // This implies that a nonWorkDay cannot be a weekend due to the calculations, above.
  53:     if (nonWorkDays != null)
  54:         foreach (DateTime nonWorkDay in nonWorkDays)
  55:             if (nonWorkDay.Subtract(nonWorkDay.TimeOfDay) >= startDate && nonWorkDay.Subtract(nonWorkDay.TimeOfDay) <= endDate)
  56:                 workDays--;
  57:  
  58:     return workDays;
  59: }

I also created a version as an MS SQL Server function so that data I pull out of the database can have counts in them and, more importantly, I can use the calculation in a WHERE clause.

   1: set ANSI_NULLS ON
   2: set QUOTED_IDENTIFIER ON
   3: go
   4:  
   5: ALTER FUNCTION [dbo].[CalculateWorkdaysCount]
   6: (
   7:     @refCalendarTypeID int = null,
   8:     @startDate datetime,
   9:     @endDate datetime
  10: )
  11: RETURNS int
  12: AS
  13: BEGIN
  14:  
  15:     -- Note: If @@DATEFIRST != 7, this function won't work
  16:  
  17:     declare @sdt datetime
  18:     declare @edt datetime
  19:  
  20:     if (@startDate is null or @endDate is null)
  21:         return 0
  22:  
  23:     set @sdt = CAST(CAST(MONTH(@startDate) as varchar) + '/' + CAST(DAY(@startDate) as varchar) + '/' + CAST(YEAR(@startDate) as varchar) as datetime)
  24:     set @edt = CAST(CAST(MONTH(@endDate) as varchar) + '/' + CAST(DAY(@endDate) as varchar) + '/' + CAST(YEAR(@endDate) as varchar) as datetime)
  25:  
  26:     if (@sdt > @edt)
  27:         return 0
  28:  
  29:     declare @workDays int
  30:     declare @weeks int
  31:  
  32:     set @workDays = DATEDIFF(day, @sdt, @edt) + 1
  33:     set @weeks = @workDays / 7
  34:     set @workDays = @workDays - (@weeks * 2)
  35:     set @edt = DATEADD(day, -@weeks * 7, @edt)
  36:  
  37:     if @edt > @sdt
  38:  
  39:     begin
  40:  
  41:         declare @sdtWeekday int
  42:         declare @edtWeekday int
  43:  
  44:         set @sdtWeekday = DATEPART(weekday, @sdt)
  45:         set @edtWeekday = DATEPART(weekday, @edt)
  46:         
  47:         if (@sdtWeekDay > @edtWeekDay and @edtWeekDay < 7 and @edtWeekDay > 1)
  48:             set @workDays = @workDays - 2
  49:         else
  50:  
  51:         begin
  52:  
  53:             if (@sdtWeekday = 7 or @sdtWeekDay = 1)
  54:                 set @workDays = @workDays - 1
  55:  
  56:             if (@edtWeekDay = 7 or @edtWeekDay = 1)
  57:                 set @workDays = @workDays - 1
  58:  
  59:         end
  60:  
  61:     end
  62:  
  63:     if @refCalendarTypeID is not null
  64:  
  65:     begin
  66:  
  67:         declare @nonWorkDayCount int
  68:  
  69:         select
  70:             @nonWorkDayCount = COUNT(*)
  71:         from
  72:             Calendars c
  73:         where
  74:             c.RefCalendarTypeID = @refCalendarTypeID
  75:             and c.NonWorkDayDate between @startDate and @endDate
  76:  
  77:         set @workDays = @workDays - @nonWorkDayCount
  78:  
  79:     end
  80:  
  81:     return @workDays
  82:  
  83: END

How It Works

The function takes a start date, end date, and an array of non-work days. The non-work days parameter can be NULL. The function first figures out the number of days between the two dates. Then, it figures out the number of full weeks between the two days and subtracts out 2 * the number of weeks from the previously calculated total days. It then subtracts the number of days in the full weeks from the end date, which will leave us with the partial week that is at either the beginning or end of the range. Then a little checking is done to see if this period spans a weekend and starts or ends on a weekend day, and depending on what it figures out, the appropriate amount is subtracted from the running total. Lastly, it runs through the non-working days checking if any occur between the start and end dates, and subtracts those that do from the running count.

The T-SQL function expects there to be a table named Calendars with two fields: RefCalendarTypeID (int) and NonWorkDayDate (smalldatetime). This is because you can't exactly pass in an array of non-work days like you can in the C# function. Just add your non-work days (e.g. holidays) to the Calendars table, and the function will calculate the number of work days between two valid dates. If you don't need that functionality, you can take out that portion at the end of the function. You'll notice the function also takes a parameter @refCalendarTypeID. This corresponds to the RefCalendarTypeID field in the Calendars table and enables you to have more than one calendar of non-work days.

Caveats

There are a few caveats that you need to be aware of. If you define a non-work day in the non-work days array (or table in the T-SQL function, below) that falls on a weekend it will be counted twice. Also, if you're working on a project or in an area where weekends are work days (or either of Saturday or Sunday) this function won't work, but you can of course tweak it to do what you want. One last thing: in the T-SQL function, as the comment states, @@DATEFIRST must be equal to 7 or it won't work (see SET DATEFIRST in the SQL Server BOL).

Conclusion

I like this method better than the numerous methods involving filling a database table with each day of the year because the maintenance is reduced. It will degrade nicely if there are no non-work days defined and it doesn't cause the calendar table to be filled with almost 2,000 rows after five years in production (not that 2,000 rows is a lot, but if we use 10 holidays per year as non-work days, that's only 50 rows with this implementation).


* If you use Reflector to check out the DateDiff function, you'll see that all it does is divide the total days between the two dates by seven and return that as a long when you pass in a DateInterval of Weekday. I dont think I have to say that this won't work for our purposes (frankly, I'm at a loss for a situation in which this would work).

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Related posts

Comments

February 2, 2009 6:51 AM

ndl

Hello. I found an inaccuracy in your code. i.e. - counting work days of 2009.05 (May) - i get 22 days and actually there is only 21. Same with 2010.01 (January) - code tells it's 22, but January has only 21. I found where the bug was occuring. When a month starts at friday and ends at Sunday, code misses one weekend day.

I added some code after line 46:

if (sdt.DayOfWeek == DayOfWeek.Friday && edt.DayOfWeek == DayOfWeek.Sunday)
workDays--;

And now it works fine.

Best regards, Paul.

ndl

March 26, 2009 3:47 PM

Mozy

This seems to be more efficient for T-SQL code at least.

stackoverflow.com/.../count-work-days-between-two-dates-in-t-sql

Mozy

May 25, 2009 4:21 AM

kpp

Thanks Bob,

The code helped quite a lot to save time.

I found a mistake in the code. When I pass a date from sunday to next sunday or saturday to next saturday, it was returning 6 working days, when it is supposed to be 5 days. I tried with a small bug fixing as below:

Modified the line : 36

if (sdt.DayOfWeek > edt.DayOfWeek && edt.DayOfWeek < DayOfWeek.Saturday && edt.DayOfWeek >= DayOfWeek.Sunday)

Added the code after line : 48

else if (edt == sdt)
{
if (edt.DayOfWeek == DayOfWeek.Sunday || edt.DayOfWeek == DayOfWeek.Saturday)
{
workDays--;
}
}


Thanx

kpp

October 30, 2009 7:57 AM

cash loans

Thank you for your help!

cash loans

November 14, 2009 3:00 AM

payday loans

Just wanted to say thanks for this.

payday loans

November 29, 2009 11:13 AM

Youdoodoll

You got a really useful blog. Thank you for sharing your thoughts on this. Bookmarked! Outstanding, Mattie Kramer @ pocketpeople

Youdoodoll

December 2, 2009 10:17 PM

Article Submission

Hello, just wanted you to know I have saved your blog to my bookmarks because of your fantastic blog layout (LOL). With that said, seriously, I think your site has one of the cleanest design I've seen yet. It really helps make reading your blog a lot easier.

Article Submission

December 3, 2009 11:33 PM

cash advance

better that reading about it try for yourself

cash advance

December 5, 2009 4:04 AM

Peridot Earrings Review

You really make it seem so easy with your presentation but I find this topic to be really something which I think I would never understand. It seems too complicated and very broad for me. I am looking forward for your next post, I will try to get the hang of it!

Peridot Earrings Review

December 5, 2009 6:02 PM

How to Get Rid of Hemorrhoids

Just wanted you to know I have added your blog to my bookmarks.

I've check out your other blog posts as well and I {can say|think| you've got great ideas|content. Keep it up|going!

How to Get Rid of Hemorrhoids

December 7, 2009 5:32 AM

puzzle games

I really enyojed this brilliant blog. Please keep them coming. Best Regards..

puzzle games

December 10, 2009 5:09 AM

faxless payday loans

Do you make money out of this blog? just curious

faxless payday loans

December 10, 2009 7:51 AM

flower

Simply, admirable what you have done here. It is fabulous to see you verbalize from the heart and your clarity on this significant subject can be easily seen. Fantastic post and will look forward to your incoming update.

flower

December 11, 2009 4:38 AM

trikapalanet

Advantageously, the post is really the greatest on this worthw hile topic. I agree with your conclusions and will thirstily look forward to your approaching updates. Saying thanks will not just be adequate, for the exceptional lucidity in your writing. I will instantly grab your rss feed to stay privy of any updates. Pleasant work and much success in your business dealings!

trikapalanet

December 11, 2009 4:56 AM

Andre A. Sparks

I am truly impressed with the general content of your blog. It is obvious that you know you topic and you are passionate about it. I wish I had got your ability to write. I have bookmarked your site and look forward to additional updates.

Andre A. Sparks

December 11, 2009 1:29 PM

red wine extract

Salut!, This site is first-class and so is how the theme was written about. I like some of the comments as well though I would rather everyone stays on the subject matter so that to add value to the point. It will be also encouraging to the author if we all could pass it around (for some of you who use social media such as a reddit, twitter,..). Thanks again.

red wine extract

December 11, 2009 6:30 PM

Mike Quick

First, let me commend your pellucidity on this subject. I am not an expert on this matter, but after reading your article, my understanding has developed substantially. Please tolerate me to catch your rss feed to remain in touch with any upcoming updates. Pleasant job and will offer it on to friends and my blog readers.

Mike Quick

December 12, 2009 6:40 AM

Free Movies

Substantially, the post is in reality the sweetest on this precious topic. I harmonize with your conclusions and will thirstily look forward to your coming updates. Saying thanks will not just be enough, for the tremendous clarity in your writing. I will right away grab your rss feed to stay abreast of any updates. Authentic work and much success in your business enterprise!

Free Movies

December 14, 2009 7:05 PM

soldes

Comfortably, the post is in reality the freshest on this notable topic. I agree with your conclusions and will eagerly look forward to your next updates. Saying thanks will not just be sufficient, for the phenomenal clarity in your writing. I will directly grab your rss feed to stay informed of any updates. Good work and much success in yourbusiness efforts!

soldes

December 16, 2009 7:59 PM

Craig Russell

thinking is flawed here, sorry but i dont agree

Craig Russell

December 18, 2009 9:44 AM

Golf Club Sets   Useful Shopping Tips For Golfers

Comfortabl y, the article is really the best on this deserving topic. I fit in with your conclusions and will thirstily look forward to your next updates. Just saying thanks will not just be sufficient, for the tremendous clarity in your writing. I will at once grab your rss feed to stay abreast of any updates. Fabulous work and much success in your business enterprize!

Golf Club Sets Useful Shopping Tips For Golfers

December 18, 2009 11:59 AM

Bread Maker   Useful Tips on How to Make the Best Bread

Advantageously, the article is actually the greatest on this notable topic. I harmonise with your conclusions and will thirstily look forward to your forthcoming updates. Saying thanks will not just be sufficient, for the great lucidity in your writing. I will at once grab your rss feed to stay abreast of any updates. De lightful work and much success in your business endeavors!

Bread Maker Useful Tips on How to Make the Best Bread

December 18, 2009 4:13 PM

Holiday Cooking Tips

You really make it seem so easy with your presentation but I find this topic to be really something which I think I would never understand. It seems too complicated and very broad for me. I am looking forward for your next post, I will try to get the hang of it!

Holiday Cooking Tips

December 19, 2009 3:33 PM

resveratrol supplements

Seasons greetings to all of you and best wishes for the coming year 2010. Great way to explain it. Can't say more than to appreciate what you are penned down. can you show how to grab your rss feed? I couldn't find how.

resveratrol supplements

December 20, 2009 5:22 PM

Brandy Lincicum

I heard there is a new solitaire game. My close friend told me about it, and I really think it's the best solitaire game ever made! I got it here: <a href="http://www.funsolitairegame.com/">Try" rel="nofollow">http://www.funsolitairegame.com/">Try It Now</a> http://www.funsolitairegame.com/

Brandy Lincicum

December 21, 2009 1:00 AM

computer repair milford

I thought you would want to know that this site does not display right on my mobile (iphone).

computer repair milford

December 21, 2009 8:24 AM

us online casinos

You may have not intended to do so, but I think you have managed to express the state of mind that a lot of people are in. The sense of wanting to help, but not knowing how or where, is something a lot of us are going through.

us online casinos