By using this site, you agree to our updated Privacy Policy and our Terms of Use. Manage your Cookies Settings.
424,503 Members | 2,730 Online
Bytes IT Community
Submit an Article
Got Smarts?
Share your bits of IT knowledge by writing an article on Bytes.

Adding business days with configurable weekend days and holidays

Seth Schrock
Expert 2.5K+
P: 2,931
Introduction
While I have been able to find an example of how to calculate the number of business days between two dates, I haven't been able to find a published method to add business days. Excel has recently added this function, but Microsoft has yet to give it to the Access world. So, I have undertaken this challenge. The function that I have created allows the user to specify what days are not included in the count (for weekends) and it also allows for a list of holidays to be excluded as well. Holidays must be entered in a table for this to work. I have created my own method for automatically getting this information from the Internet, but that is a subject for another post.

Tables
The best method that I could come up with was to have a list of available dates and then to count down the records for the specified number of days. This means that we need a list of dates. Instead of having to type them all into a table, we will let Access take care of this problem. All we need is a table with a single field in it:
Expand|Select|Wrap|Line Numbers
  1. tbl_ExpansionDigits
  2. Digit
Now add 10 records with the values 0 to 9

We also need a table of holidays. My table has a date field and a name field so that I can list the holiday name if I want to.
Expand|Select|Wrap|Line Numbers
  1. dbo_Holidays
  2. hDate
  3. hName
That is it for tables

Queries
We actually only need one query. This is what performs the magic of giving us dates. Mine is named qryPossibleDates.
Expand|Select|Wrap|Line Numbers
  1. SELECT Cdate([tbl_expansiondigits_1]![Digit]
  2.    +[tbl_expansiondigits_2]![Digit]*10
  3.    +[tbl_expansiondigits_3]![Digit]*100
  4.    +[tbl_expansiondigits_4]![Digit]*1000 
  5.    +[tbl_expansiondigits_5]![Digit]*10000) AS CalcDate, Weekday(CalcDate) AS WeekDayNumber
  6. FROM tbl_expansiondigits AS tbl_expansiondigits_1, tbl_expansiondigits AS tbl_expansiondigits_2, tbl_expansiondigits AS tbl_expansiondigits_3, tbl_expansiondigits AS tbl_expansiondigits_4, tbl_expansiondigits AS tbl_expansiondigits_5
  7. WHERE ((([tbl_expansiondigits_1]![Digit]
  8.    +[tbl_expansiondigits_2]![Digit]*10
  9.    +[tbl_expansiondigits_3]![Digit]*100
  10.    +[tbl_expansiondigits_4]![Digit]*1000
  11.    +[tbl_expansiondigits_5]![Digit]*10000)>=36000));
This gives us dates from 7/24/1998 to 10/13/2173 (a total of 64,000 days) plus the weekday number (1 for Sunday, 7 for Saturday).


Code
First we are going to need a function to convert a binary-like number to a decimal value. I use this to for binary flags to know which days to exclude from the count.
Expand|Select|Wrap|Line Numbers
  1. Private Function ConvertToDecimal(lngBinary As Long) As Long
  2. Dim strNumber As String
  3. Dim i As Integer
  4. Dim lngAccumulator As Long
  5. Dim n As Integer
  6. Dim s As String
  7. Dim p As Integer
  8.  
  9. strNumber = Format(lngBinary, "00000000")
  10. p = 1
  11. For i = 8 To 1 Step -1
  12.     s = Mid(strNumber, p, 1)
  13.     If s = "1" Then
  14.         n = CInt(i) - 1
  15.         lngAccumulator = lngAccumulator + 2 ^ n
  16.     End If
  17.     p = p + 1
  18. Next
  19.  
  20. ConvertToDecimal = lngAccumulator
  21.  
  22. End Function
Now for the main function.
Expand|Select|Wrap|Line Numbers
  1. Public Function AddDays(dteStart As Date, intInterval As Integer, lngExcludePattern As Long) As Date
  2. Dim lngExcludeDates As Long
  3. Dim strSelect As String
  4. Dim strWhere As String
  5. Dim strSQL As String
  6. Dim db As DAO.Database
  7. Dim rst As DAO.Recordset
  8. Dim i As Integer
  9.  
  10. If lngExcludePattern <> 0 Then
  11.     lngExcludeDates = ConvertToDecimal(lngExcludePattern)
  12.     strSelect = "SELECT CalcDate As AddedDate FROM qryPossibleDates"
  13.     strWhere = "CalcDate = #" & dteStart & "# Or (CalcDate >= #" & dteStart & "# And CalcDate Not In(SELECT hDate FROM dbo_Holidays WHERE hDate >= #" & dteStart & "#)"
  14.  
  15.     If (lngExcludeDates And 64) > 0 Then
  16.         strWhere = strWhere & " And WeekDayNumber <> 1"
  17.     End If
  18.  
  19.     If (lngExcludeDates And 32) > 0 Then
  20.         strWhere = strWhere & " And WeekDayNumber <> 2"
  21.     End If
  22.  
  23.     If (lngExcludeDates And 16) > 0 Then
  24.         strWhere = strWhere & " And WeekDayNumber <> 3"
  25.     End If
  26.  
  27.     If (lngExcludeDates And 8) > 0 Then
  28.         strWhere = strWhere & " And WeekDayNumber <> 4"
  29.     End If
  30.  
  31.     If (lngExcludeDates And 4) > 0 Then
  32.         strWhere = strWhere & " And WeekDayNumber <> 5"
  33.     End If
  34.  
  35.     If (lngExcludeDates And 2) > 0 Then
  36.         strWhere = strWhere & " And WeekDayNumber <> 6"
  37.     End If
  38.  
  39.     If (lngExcludeDates And 1) > 0 Then
  40.         strWhere = strWhere & " And WeekDayNumber <> 7"
  41.     End If
  42.  
  43.     strWhere = strWhere & ")"
  44.     strSQL = strSelect & " WHERE " & strWhere
  45.     Debug.Print strSQL
  46.  
  47.     Set db = CurrentDb
  48.     Set rst = db.OpenRecordset(strSQL, dbOpenDynaset, dbSeeChanges)
  49.  
  50.     rst.Move intInterval
  51.  
  52.     AddDays = rst!AddedDate
  53.  
  54.     Set db = Nothing
  55.     rst.Close
  56.     Set rst = Nothing
  57.  
  58. Else
  59.     AddDays = dteStart + intInterval
  60. End If
  61.  
  62. End Function
First, I test for the value in lngExcludePattern. If it is 0, then I'm just counting calendar days and it skips to the bottom. Otherwise, I build a query string that pulls from the query above adding the criteria. You will notice that it starts with CalcDate = #" & dteStart & "#". This is for situations where dteStart is not a day that is included in the query as it does mess with the days involved (I found this out after troubleshooting why my counts were off). It then has the criteria to only show records that are greater than or equal to dteStart and not in the list of holidays, again filtered to be greater than or equal to dteStart (for performance). I then test for the binary flags present to add additional criteria to exclude the days of the week that we don't want included. I then open the recordset and use the .Move method to go to the correct record in the recordset to get my date. And that is it!

Use
To use this function, you pass the start date to which you want to add x number of days (dteStart), the number of days that you want to add (intInterval) and then the days that you want to exclude (lngExcludePattern). The first two are pretty simple, but the Exclude Pattern needs more explanation. I have it setup to do it as binary flags. The first flag is Sunday and the last flag is Saturday. So if I want my weekends to be Saturday and Sunday, my Exclude Pattern would be 1000001. If I want Wednesdays excluded as well, then my pattern would be 1001001. If I just want Saturdays excluded it would be 0000001, which can also be entered as 1 because it is passed as a long integer. So if I want to add 5 days to 1/17/16, excluding Saturdays and Sundays, I would call my function like this:
Expand|Select|Wrap|Line Numbers
  1. =AddDays(#1/17/16#, 5, 1000001)

Conclusion
There are probably other ways to do this, but this seemed like the most versatile method and it actually does what I want it to. If anyone has any critiques, I welcome them.
Jan 15 '16 #1
Share this Article
Share on Google+
4 Comments


Narender Sagar
100+
P: 189
Hi
It may be a silly question.
But I'm not able to figure out, how to use this final code?
Jan 16 '16 #2

Seth Schrock
Expert 2.5K+
P: 2,931
It isn't a silly question, but a oversight on my part. I have added a Use section to my original post. Does that provide enough of an answer for you, or do I need to explain in more detail?
Jan 17 '16 #3

P: 1
Hello,

that's very kind of you sharing this code. I am a novice user but manage to implement it, but...here, from Catalonia (European dates as dd/mm/yyyy), the codes works fine while the date matches my Access installation (dd/mm/yyyy). I mean that Access decides that 13 cannot be a month and treats this 13/01/2016 as properly (for me) whereas below 13, any date in the same list is treated as mm/dd/yyyy and the AddDays function add the interval to months instead of days.
Please, could you give some clues on how to fix it?
Thank you very much!!!
Jul 28 '16 #4

zmbd
Expert Mod 5K+
P: 5,287
jordiseco: Access works internally with the #dd/mm/yyyy# format - read more about that here:
+ Allen Browne - International Dates in Access
and here
+ Bytes.Com - Literal DateTimes and Their Delimiters (#)

Often Access will correctly recognize the local regional date format; however, to ensure things are correctly interpreted internally one should use the "#" and the mm/dd/yyyy formatting for VBA and SQL.

:)
Aug 2 '16 #5