I'm attempting to write a formula that rounds an end date it to the nearest workday given the start date. I will want the flexibility to add the number of days to start date. For example, if I have the dates November 27, 2021 (which is a Saturday) and November 28, 2021 (which is a Sunday) I want the formula to return November 29, 2021 (Monday). However, if the date November 26, 2021 return same date since it’s a working day. The date will also move to the next working day if the Date is a holiday. Thanks
Public Function AddDueDate(StartDate As Date, TotalPeriold As Integer) As Date
Dim rst As Recordset
Dim db As Database
Dim Duedate As Date
Dim icount As Integer
On Error GoTo errhandlers:
Set db = CurrentDb
Set rst = db.OpenRecordset("tblHolidays", dbOpenSnapshot)
icount = 0
Duedate = StartDate
Do While icount < TotalPeriod
Duedate = Duedate + 1
If Weekday(Duedate, vbMonday) < 6 Then
rst.FindFirst "[Holidaydate]=#" & Duedate & "#"
If rst.NoMatch Then
icount = icount + 1
End If
End If
Loop
AddDueDate = Duedate
exit_errhandlers:
rst.Close
Set rst = Nothing
Set db = Nothing
Exit Function
errhandlers:
MsgBox Err.Description, vbExclamation
Resume Next
End Function
You can obtain that with a combo of my functions found in my project at VBA.Date:
WorkdayDate = DateAddWorkdays(Abs(Not IsDateWorkday(YourDate)), YourDate)
which will add zero days to a date of a workday but, for a non-workday, return the following date of workday.
Full code is too much to post here, but this is the core function:
' Adds Number of full workdays to Date1 and returns the found date.
' Number can be positive, zero, or negative.
' Optionally, if WorkOnHolidays is True, holidays are counted as workdays.
'
' For excessive parameters that would return dates outside the range
' of Date, either 100-01-01 or 9999-12-31 is returned.
'
' Will add 500 workdays in about 0.01 second.
'
' Requires table Holiday with list of holidays.
'
' 2021-12-09. Gustav Brock. Cactus Data ApS, CPH.
'
Public Function DateAddWorkdays( _
ByVal Number As Long, _
ByVal Date1 As Date, _
Optional ByVal WorkOnHolidays As Boolean) _
As Date
Const Interval As String = "d"
Dim Holidays() As Date
Dim Days As Long
Dim DayDiff As Long
Dim MaxDayDiff As Long
Dim Sign As Long
Dim Date2 As Date
Dim NextDate As Date
Dim DateLimit As Date
Dim HolidayId As Long
Sign = Sgn(Number)
NextDate = Date1
If Sign <> 0 Then
If WorkOnHolidays = True Then
' Holidays are workdays.
Else
' Retrieve array with holidays between Date1 and Date1 + MaxDayDiff.
' Calculate the maximum calendar days per workweek.
If (WorkDaysPerWeek - HolidaysPerWeek) > 1 Then
MaxDayDiff = Number * DaysPerWeek / (WorkDaysPerWeek - HolidaysPerWeek)
Else
MaxDayDiff = Number * DaysPerWeek
End If
' Add one week to cover cases where a week contains multiple holidays.
MaxDayDiff = MaxDayDiff + Sgn(MaxDayDiff) * DaysPerWeek
If Sign > 0 Then
If DateDiff(Interval, Date1, MaxDateValue) < MaxDayDiff Then
MaxDayDiff = DateDiff(Interval, Date1, MaxDateValue)
End If
Else
If DateDiff(Interval, Date1, MinDateValue) > MaxDayDiff Then
MaxDayDiff = DateDiff(Interval, Date1, MinDateValue)
End If
End If
Date2 = DateAdd(Interval, MaxDayDiff, Date1)
' Retrive array with holidays.
Holidays = DatesHoliday(Date1, Date2)
End If
Do Until Days = Number
If Sign = 1 Then
DateLimit = MaxDateValue
Else
DateLimit = MinDateValue
End If
If DateDiff(Interval, DateAdd(Interval, DayDiff, Date1), DateLimit) = 0 Then
' Limit of date range has been reached.
Exit Do
End If
DayDiff = DayDiff + Sign
NextDate = DateAdd(Interval, DayDiff, Date1)
Select Case Weekday(NextDate)
Case vbSaturday, vbSunday
' Skip weekend.
Case Else
' Check for holidays to skip.
' Ignore error when using LBound and UBound on an unassigned array.
On Error Resume Next
For HolidayId = LBound(Holidays) To UBound(Holidays)
If Err.Number > 0 Then
' No holidays between Date1 and Date2.
ElseIf DateDiff(Interval, NextDate, Holidays(HolidayId)) = 0 Then
' This NextDate hits a holiday.
' Subtract one day before adding one after the loop.
Days = Days - Sign
Exit For
End If
Next
On Error GoTo 0
Days = Days + Sign
End Select
Loop
End If
DateAddWorkdays = NextDate
End Function
Related
I am using the DateAddWorkdays in my access database that has a SQL backend which works great, most of the time. But I am having some odd inconsistencies and I am not sure why. I have stepped through the script for the past few hours and can't see where the issue is coming from.
I have a table of dates that includes 25th October 2021 as a holiday:
I am using HolidayDate field.
I call the function with
Me.ProductionDate = DateAddWorkdays(-Me.Lag, Me.DelDate)
The number is a negative as I need to count back the number of lag days to know when to start producing the items in time for the delivery date (me.DelDate).
As below, if there are only 2 days it counts back perfectly to 21st October as it removes the 25th being the holiday and the 23rd & 24th as they are weekend days. But if the Lag is 6 days it misses a day somewhere and returns the 18th where I would expect it should return the previous friday the 15th (to test this theory, if I change the delivery date to 27th October it also returns the 18th as a result)
As much as I stepped through the script, I don't fully understand what it is doing so I am hoping someone is able to show me why I am getting this inconsistency.
I have been fiddling around with some date formatting as that was causing issues elsewhere but this script seems to have that incorporated.
Option Compare Database
Option Explicit
Public Function DateAddWorkdays( _
ByVal lngNumber As Long, _
ByVal datDate As Date, _
Optional ByVal booWorkOnHolidays As Boolean) _
As Date
' Adds lngNumber of workdays to datDate.
' 2014-10-03. Cactus Data ApS, CPH
' Calendar days per week.
Const clngWeekdayCount As Long = 7
' Workdays per week.
Const clngWeekWorkdays As Long = 5
' Average count of holidays per week maximum.
Const clngWeekHolidays As Long = 1
' Maximum valid date value.
Const cdatDateRangeMax As Date = #12/31/9999#
' Minimum valid date value.
Const cdatDateRangeMin As Date = #1/1/100#
Dim aHolidays() As Date
Dim lngDays As Long
Dim lngDiff As Long
Dim lngDiffMax As Long
Dim lngSign As Long
Dim datDate1 As Date
Dim datDate2 As Date
Dim datLimit As Date
Dim lngHoliday As Long
lngSign = Sgn(lngNumber)
datDate2 = datDate
If lngSign <> 0 Then
If booWorkOnHolidays = True Then
' Holidays are workdays.
Else
' Retrieve array with holidays between datDate and datDate + lngDiffMax.
' Calculate the maximum calendar days per workweek.
lngDiffMax = lngNumber * clngWeekdayCount / (clngWeekWorkdays - clngWeekHolidays)
' Add one week to cover cases where a week contains multiple holidays.
lngDiffMax = lngDiffMax + Sgn(lngDiffMax) * clngWeekdayCount
datDate1 = DateAdd("d", lngDiffMax, datDate)
aHolidays = GetHolidays(datDate, datDate1)
End If
Do Until lngDays = lngNumber
If lngSign = 1 Then
datLimit = cdatDateRangeMax
Else
datLimit = cdatDateRangeMin
End If
If DateDiff("d", DateAdd("d", lngDiff, datDate), datLimit) = 0 Then
' Limit of date range has been reached.
Exit Do
End If
lngDiff = lngDiff + lngSign
datDate2 = DateAdd("d", lngDiff, datDate)
Select Case Weekday(datDate2)
Case vbSaturday, vbSunday
' Skip weekend.
Case Else
' Check for holidays to skip.
' Ignore error when using LBound and UBound on an unassigned array.
On Error Resume Next
For lngHoliday = LBound(aHolidays) To UBound(aHolidays)
If Err.Number > 0 Then
' No holidays between datDate and datDate1.
ElseIf DateDiff("d", datDate2, aHolidays(lngHoliday)) = 0 Then
' This datDate2 hits a holiday.
' Subtract one day before adding one after the loop.
lngDays = lngDays - lngSign
Exit For
End If
Next
On Error GoTo 0
lngDays = lngDays + lngSign
End Select
Loop
End If
DateAddWorkdays = datDate2
End Function
Public Function GetHolidays( _
ByVal datDate1 As Date, _
ByVal datDate2 As Date, _
Optional ByVal booDesc As Boolean) _
As Date()
' Finds the count of holidays between datDate1 and datDate2.
' The holidays are returned as an array of dates.
' DAO objects are declared static to speed up repeated calls with identical date parameters.
' 2014-10-03. Cactus Data ApS, CPH
' The table that holds the holidays.
Const cstrTable As String = "tblHoliday"
' The field of the table that holds the dates of the holidays.
Const cstrField As String = "HolidayDate"
' Constants for the arrays.
Const clngDimRecordCount As Long = 2
Const clngDimFieldOne As Long = 0
Static dbs As DAO.Database
Static rst As DAO.Recordset
Static datDate1Last As Date
Static datDate2Last As Date
Dim adatDays() As Date
Dim avarDays As Variant
Dim strSQL As String
Dim strDate1 As String
Dim strDate2 As String
Dim strOrder As String
Dim lngDays As Long
If DateDiff("d", datDate1, datDate1Last) <> 0 Or DateDiff("d", datDate2, datDate2Last) <> 0 Then
' datDate1 or datDate2 has changed since the last call.
strDate1 = Format(datDate1, "\#yyyy\/mm\/dd\#")
strDate2 = Format(datDate2, "\#yyyy\/mm\/dd\#")
strOrder = Format(booDesc, "\A\s\c;\D\e\s\c")
strSQL = "Select " & cstrField & " From " & cstrTable & " " & _
"Where " & cstrField & " Between " & strDate1 & " And " & strDate2 & " " & _
"Order By 1 " & strOrder
Set dbs = CurrentDb
Set rst = dbs.OpenRecordset(strSQL, dbOpenSnapshot)
' Save the current set of date parameters.
datDate1Last = datDate1
datDate2Last = datDate2
End If
lngDays = rst.RecordCount
If lngDays = 0 Then
' Leave adatDays() as an unassigned array.
Else
ReDim adatDays(lngDays - 1)
' As repeated calls may happen, do a movefirst.
rst.MoveFirst
avarDays = rst.GetRows(lngDays)
' rst is now positioned at the last record.
For lngDays = LBound(avarDays, clngDimRecordCount) To UBound(avarDays, clngDimRecordCount)
adatDays(lngDays) = avarDays(clngDimFieldOne, lngDays)
Next
End If
' DAO objects are static.
' Set rst = Nothing
' Set dbs = Nothing
GetHolidays = adatDays()
End Function
Further to my question in the comments below, I have come across another example where the calculation is inconsistent. Above is a delivery date of 11/01/2022 which is the 11th January 2022 and with a lag of 2 days it calculates correctly including the holidays from the table below. But a lag anywhere between 9 and 14 and it seems to ignore the holidays but as long as the lag is less than 9 or more than 14 it calculates correctly.
The error is here, where the extended range for possible holidays didn't follow the sign of the date interval, which - in your case - caused the holiday to be excluded:
' Retrieve array with holidays between datDate and datDate + lngDiffMax.
' Calculate the maximum calendar days per workweek.
lngDiffMax = lngNumber * clngWeekdayCount / (clngWeekWorkdays - clngWeekHolidays)
' Add one week to cover cases where a week contains multiple holidays.
'' Missing sign.
'' lngDiffMax = lngDiffMax + clngWeekdayCount
'' Corrected to follow the sign of the date interval.
lngDiffMax = lngDiffMax + Sgn(lngDiffMax) * clngWeekdayCount
datDate1 = DateAdd("d", lngDiffMax, datDate)
aHolidays = GetHolidays(datDate, datDate1)
Now you can run this quick test:
For n = 0 To 7 : ? d, -n, DateAddWorkdays(-n, d) : Next
26-10-2021 0 26-10-2021
26-10-2021 -1 22-10-2021
26-10-2021 -2 21-10-2021
26-10-2021 -3 20-10-2021
26-10-2021 -4 19-10-2021
26-10-2021 -5 18-10-2021
26-10-2021 -6 15-10-2021
26-10-2021 -7 14-10-2021
The function has had a make-over and is included in my project VBA.Date in module DateWork.bas. (Disclaimer: Project holds extensive code written by me).
Extended holidays
Increase the maximum count of possible holidays per week to the count of workdays per week minus one:
' Common constants.
' Workdays per week.
Public Const WorkDaysPerWeek As Long = 5
' Average count of holidays per week maximum.
Public Const HolidaysPerWeek As Long = 4 '1
Now your sample will run like this:
Lag Production Date
--- ---------------
1 10-01-2022
2 21-12-2021
3 20-12-2021
4 17-12-2021
5 16-12-2021
6 15-12-2021
7 14-12-2021
8 13-12-2021
9 10-12-2021
10 09-12-2021
11 08-12-2021
12 07-12-2021
13 06-12-2021
14 03-12-2021
15 02-12-2021
16 01-12-2021
I have the following table:
I somehow managed to write a query which allowed me to sum and group the total of visitors to a zoo per day (as the table), but I need to group even further into weeks in order to calculate the percentage of growth of the amount of visitors week over week for the last four weeks of the data.
How can I do that in Access SQL?
This is the query:
SELECT TOP 35 date_info.calendar_date, Sum(restaurants_visitors.reserve_visitors) AS SUMreserve_visitors
FROM date_info INNER JOIN restaurants_visitors ON date_info.calendar_date = restaurants_visitors.visit_date
GROUP BY date_info.calendar_date
ORDER BY date_info.calendar_date DESC;
Access stores dates as a floating point number whose integer part represents days since 1900 and whose fractional part is the time expressed as a fraction of a day. If I divide the date by 7 and truncate to integer I will have a week number spanning Saturday to Friday. Put that in a Group By and you're good to go.
Group By Fix(date_info.calendar_date/7)
First, you need a function to calculate the week number. Also the year of the week numbers, as weeks can span New Year.
This you can format like 2021W23 to list orderly:
' Returns, for a date value, a formatted string expression with
' year and weeknumber according to ISO-8601.
' Optionally, a W is used as separator between the year and week parts.
'
' Typical usage:
'
' FormatWeekIso8601(Date)
' -> 2017-23
'
' FormatWeekIso8601(Date, True)
' -> 2017W23
'
' 2017-04-28. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function FormatWeekIso8601( _
ByVal Expression As Variant, _
Optional ByVal WSeparator As Boolean) _
As String
Const Iso8601Separator As String = "W"
Const NeutralSeparator As String = "-"
Dim Result As String
Dim IsoYear As Integer
Dim IsoWeek As Integer
If IsDate(Expression) Then
IsoWeek = Week(DateValue(Expression), IsoYear)
Result = _
VBA.Format(IsoYear, String(3, "0")) & _
IIf(WSeparator, Iso8601Separator, NeutralSeparator) & _
VBA.Format(IsoWeek, String(2, "0"))
End If
FormatWeekIso8601 = Result
End Function
' Returns the ISO 8601 week of a date.
' The related ISO year is returned by ref.
'
' 2016-01-06. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function Week( _
ByVal Date1 As Date, _
Optional ByRef IsoYear As Integer) _
As Integer
Const MaxMonthValue As Integer = 12
Const MinMonthValue As Integer = 1
Const MaxWeekValue As Integer = 53
Const MinWeekValue As Integer = 1
Dim Month As Integer
Dim Interval As String
Dim Result As Integer
Interval = "ww"
Month = VBA.Month(Date1)
' Initially, set the ISO year to the calendar year.
IsoYear = VBA.Year(Date1)
Result = DatePart(Interval, Date1, vbMonday, vbFirstFourDays)
If Result = MaxWeekValue Then
If DatePart(Interval, DateAdd(Interval, 1, Date1), vbMonday, vbFirstFourDays) = MinWeekValue Then
' OK. The next week is the first week of the following year.
Else
' This is really the first week of the next ISO year.
' Correct for DatePart bug.
Result = MinWeekValue
End If
End If
' Adjust year where week number belongs to next or previous year.
If Month = MinMonthValue Then
If Result >= MaxWeekValue - 1 Then
' This is an early date of January belonging to the last week of the previous ISO year.
IsoYear = IsoYear - 1
End If
ElseIf Month = MaxMonthValue Then
If Result = MinWeekValue Then
' This is a late date of December belonging to the first week of the next ISO year.
IsoYear = IsoYear + 1
End If
End If
' IsoYear is returned by reference.
Week = Result
End Function
Now you can create a query like this to list the visitor counts from the last week and four weeks back:
SELECT
FormatWeekIso8601(date_info.calendar_date) As Year_Week,
Sum(restaurants_visitors.reserve_visitors) AS SUMreserve_visitors
FROM
date_info
INNER JOIN
restaurants_visitors
ON date_info.calendar_date = restaurants_visitors.visit_date
GROUP BY
FormatWeekIso8601(date_info.calendar_date)
HAVING
FormatWeekIso8601(date_info.calendar_date) Between
FormatWeekIso8601(DateAdd("ww", -4, date_info.calendar_date)) And
FormatWeekIso8601(DateAdd("ww", -1, date_info.calendar_date))
ORDER BY
FormatWeekIso8601(date_info.calendar_date) DESC;
Source: VBA.Date
I am trying to create a simple week selector based on the ISO Week number, which will give me the Monday date and the Sunday date every time the user clicks on "Current Week" or "Previous Week" or "Next Week" Buttons, as I will select all the transactions within those dates.
I have managed doing that following the steps from
MS Access get ISO standard week number
to get the correct week number for a specific date, and then converting the week number back to date following https://answers.microsoft.com/en-us/msoffice/forum/msoffice_access-mso_other/convert-week-number-to-date/3d0f8c90-a155-e011-8dfc-68b599b31bf5.
My conversion works fine for this year, every time I click in previous or next week, it brings the correct Monday and Sunday along with its correct week number, however, when it arrives on week 1 of 2021, which brings the correct dates of 04/01/2021 and 10/01/2021 (from and to respectively), the next click on "next week" brings the dates "from = 06/01/2021" and "to = 12/01/2021", and it stops going forward, the clicks don't change the dates.
When clicking "Previous Week", it goes well till week 1 of 2020, which brings the correct dates of 30/12/2019 and 05/01/2020, but the next click on "Previous Week" brings the dates 23/12/2018 and 29/12/18, but in this case, if I continue to click in Previous Week button it continues going back into 2018 correctly. It is just mad how it occurs.
I believe that the problem is in the DateSerial when converting the Week Number to Date Range, I have tried to figure it out, but I couldn't do it.
I hope you guys can help me out.
Thank you in advance.
'''' This is the function in a module to get the week number
Public Function ISOWeek(MyDate As Date) As Integer
ISOWeek = Format(MyDate, "ww", vbMonday, vbFirstFourDays)
If ISOWeek > 52 Then
If Format(MyDate + 7, "ww", vbMonday, vbFirstFourDays) = 2 Then ISOWeek = 1
End If
End Function
'''' These subs run on the form code
Private Sub NextWeek_Click()
Dim SelectedWeek As Date
SelectedWeek = Me.Date_From.Value
FirstDayWeek = DateAdd("ww", ISOWeek(SelectedWeek), DateSerial(Year(SelectedWeek), 1, 1) - 2)
LastDayWeek = DateAdd("ww", ISOWeek(SelectedWeek), DateSerial(Year(SelectedWeek), 1, 1) + 4)
Me.Date_From.Value = FirstDayWeek
Me.Date_To.Value = LastDayWeek
End Sub
Private Sub PreviousWeek_Click()
Dim SelectedWeek As Date
SelectedWeek = Me.Date_From.Value
FirstDayWeek = DateAdd("ww", ISOWeek(SelectedWeek) - 2, DateSerial(Year(SelectedWeek), 1, 1) - 2)
LastDayWeek = DateAdd("ww", ISOWeek(SelectedWeek) - 2, DateSerial(Year(SelectedWeek), 1, 1) + 4)
Me.Date_From.Value = FirstDayWeek
Me.Date_To.Value = LastDayWeek
End Sub
Leave the week numbers from the date calculations, they only complicate matters.
By using the generic functions listed below, your two functions can be reduced to:
Private Sub NextWeek_Click()
Me.Date_From.Value = DateNextWeekPrimo(Me.Date_From.Value, vbMonday)
Me.Date_To.Value = DateNextWeekUltimo(Me.Date_From.Value, vbMonday)
End Sub
Private Sub PreviousWeek_Click()
Me.Date_From.Value = DatePreviousWeekPrimo(Me.Date_From.Value, vbMonday)
Me.Date_To.Value = DatePreviousWeekUltimo(Me.Date_From.Value, vbMonday)
End Sub
' Returns the primo date of the week following the week of the date passed.
'
' 2016-01-13. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function DateNextWeekPrimo( _
ByVal DateThisWeek As Date, _
Optional ByVal FirstDayOfWeek As VbDayOfWeek = vbSunday) _
As Date
Dim Interval As String
Dim Number As Double
Dim ResultDate As Date
Number = 1
Interval = "ww"
' Offset date.
ResultDate = DateAdd(Interval, Number, DateThisWeek)
' Return first weekday with no time part.
ResultDate = DateAdd("d", 1 - Weekday(ResultDate, FirstDayOfWeek), Fix(ResultDate))
DateNextWeekPrimo = ResultDate
End Function
' Returns the ultimo date of the week following the week of the date passed.
'
' 2016-01-13. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function DateNextWeekUltimo( _
ByVal DateThisWeek As Date, _
Optional ByVal FirstDayOfWeek As VbDayOfWeek = vbSunday) _
As Date
Dim Interval As String
Dim Number As Double
Dim ResultDate As Date
Number = 1
Interval = "ww"
' Offset date.
ResultDate = DateAdd(Interval, Number, DateThisWeek)
' Return last weekday with no time part.
ResultDate = DateAdd("d", 7 - Weekday(ResultDate, FirstDayOfWeek), Fix(ResultDate))
DateNextWeekUltimo = ResultDate
End Function
' Returns the primo date of the week preceding the week of the date passed.
'
' 2016-01-13. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function DatePreviousWeekPrimo( _
ByVal DateThisWeek As Date, _
Optional ByVal FirstDayOfWeek As VbDayOfWeek = vbSunday) _
As Date
Dim Interval As String
Dim Number As Double
Dim ResultDate As Date
Number = -1
Interval = "ww"
' Offset date.
ResultDate = DateAdd(Interval, Number, DateThisWeek)
' Return first weekday with no time part.
ResultDate = DateAdd("d", 1 - Weekday(ResultDate, FirstDayOfWeek), Fix(ResultDate))
DatePreviousWeekPrimo = ResultDate
End Function
' Returns the ultimo date of the week preceding the week of the date passed.
'
' 2016-01-13. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function DatePreviousWeekUltimo( _
ByVal DateThisWeek As Date, _
Optional ByVal FirstDayOfWeek As VbDayOfWeek = vbSunday) _
As Date
Dim Interval As String
Dim Number As Double
Dim ResultDate As Date
Number = -1
Interval = "ww"
' Offset date.
ResultDate = DateAdd(Interval, Number, DateThisWeek)
' Return last weekday with no time part.
ResultDate = DateAdd("d", 7 - Weekday(ResultDate, FirstDayOfWeek), Fix(ResultDate))
DatePreviousWeekUltimo = ResultDate
End Function
First of all, I'm a beginner and still learning VBA, thank you for your consideration.
I have a CalcWorkingDays function which which calculates working days within a specific period (period defined by a query parameter).
But when it returns results, for some periods it is completely correct, and for some others it's incorrect (See example at the end)
I guess the problem is in these lines :
If Format(DateCnt, "w") <> "7" And _
Format(DateCnt, "w") <> "6" Then
Thank you !
Public Function CalcWorkingDays(BegDate As Variant, EndDate As Variant) As Integer
Dim WholeWeeks As Variant
Dim DateCnt As Variant
Dim EndDays As Integer
On Error GoTo Err_Work_Days
BegDate = DateValue(BegDate)
EndDate = DateValue(EndDate)
WholeWeeks = DateDiff("w", BegDate, EndDate)
DateCnt = DateAdd("ww", WholeWeeks, BegDate)
EndDays = 0
Do While DateCnt <= EndDate
If Format(DateCnt, "w") <> "7" And _
Format(DateCnt, "w") <> "6" Then
EndDays = EndDays + 1
End If
DateCnt = DateAdd("d", 1, DateCnt)
Loop
CalcWorkingDays = WholeWeeks * 5 + EndDays
Exit Function
[...]
End Function`
For example, on march 2019.
there is a total of 21 working days. We have both employees A and B
A : he's on a project from 01/01/2019 to 31/12/2019, the function gives me 21 working days for march which is correct
B : He's been assigned to a project from 01/03/2019 to 08/03/2019, it gives me 5 which is incorrect, it should give me 6 (8 total days days - 2 days for week end
Harassed Dad is right - if you use Format(DateCnt, "w"), Sunday will be "1", Monday "2"...
But you shouldn't use Format to get the day of the week - Format is for formatting data into strings, and there is no need to involve strings. Use the Weekday-function instead.
The default behavior for Weekday is that Sunday will be 1 (as a number, not a string), but you can change that with the 2nd parameter (FirstDayOfWeek). This defines which day you want to have as first day of the week.
So you can change your logic for example to
If Weekday(DateCnt, vbMonday) < 6 Then
Date arithmetic is tricky. If you are not hugely concerned about efficiency and your intervals are relatively small then a really simple function will do the trick
Public Function CalcWorkingDays(BegDate As Variant, EndDate As Variant) As Integer
CalcWorkingDays = 0
For i = begdate To enddate
If Weekday(i, vbMonday) <= 5 Then
CalcWorkingDays = CalcWorkingDays + 1
End If
Next
End Function
Not particularly elegant but effective, easy to understand, and easy to modify.
The function gives me 21 working days for march which is correct B
He's been assigned to a project from 01/03/2019 to 08/03/2019, it
gives me 5 which is incorrect, it should give me 6.
A diff-function will never include the last date. If you wish to include that last date, add one day to the last date before calculating:
? DateDiffWorkDays(#2019/03/01#, #2019/03/31#)
21
? DateDiffWorkDays(#2019/03/01#, #2019/04/01#)
21
? DateDiffWorkDays(#2019/03/01#, #2019/03/08#)
5
? DateDiffWorkDays(#2019/03/01#, #2019/03/09#)
6
Also, as already noted, specify Monday as the first day of the week. Further, don't use Format; Weekday is the "direct" method. Thus:
If Weekday(DateCnt, vbMonday) < 6 Then
EndDays = EndDays + 1
End If
For an extended method that takes holidays into account, study my functions:
Option Compare Database
Option Explicit
' Returns the count of full workdays between Date1 and Date2.
' The date difference can be positive, zero, or negative.
' Optionally, if WorkOnHolidays is True, holidays are regarded as workdays.
'
' Note that if one date is in a weekend and the other is not, the reverse
' count will differ by one, because the first date never is included in the count:
'
' Mo Tu We Th Fr Sa Su Su Sa Fr Th We Tu Mo
' 0 1 2 3 4 4 4 0 0 -1 -2 -3 -4 -5
'
' Su Mo Tu We Th Fr Sa Sa Fr Th We Tu Mo Su
' 0 1 2 3 4 5 5 0 -1 -2 -3 -4 -5 -5
'
' Sa Su Mo Tu We Th Fr Fr Th We Tu Mo Su Sa
' 0 0 1 2 3 4 5 0 -1 -2 -3 -4 -4 -4
'
' Fr Sa Su Mo Tu We Th Th We Tu Mo Su Sa Fr
' 0 0 0 1 2 3 4 0 -1 -2 -3 -3 -3 -4
'
' Execution time for finding working days of three years is about 4 ms.
'
' Requires table Holiday with list of holidays.
'
' 2015-12-19. Gustav Brock. Cactus Data ApS, CPH.
'
Public Function DateDiffWorkdays( _
ByVal Date1 As Date, _
ByVal Date2 As Date, _
Optional ByVal WorkOnHolidays As Boolean) _
As Long
Dim Holidays() As Date
Dim Diff As Long
Dim Sign As Long
Dim NextHoliday As Long
Dim LastHoliday As Long
Sign = Sgn(DateDiff("d", Date1, Date2))
If Sign <> 0 Then
If WorkOnHolidays = True Then
' Holidays are workdays.
Else
' Retrieve array with holidays between Date1 and Date2.
Holidays = GetHolidays(Date1, Date2, False) 'CBool(Sign < 0))
' Ignore error when using LBound and UBound on an unassigned array.
On Error Resume Next
NextHoliday = LBound(Holidays)
LastHoliday = UBound(Holidays)
' If Err.Number > 0 there are no holidays between Date1 and Date2.
If Err.Number > 0 Then
WorkOnHolidays = True
End If
On Error GoTo 0
End If
' Loop to sum up workdays.
Do Until DateDiff("d", Date1, Date2) = 0
Select Case Weekday(Date1)
Case vbSaturday, vbSunday
' Skip weekend.
Case Else
If WorkOnHolidays = False Then
' Check for holidays to skip.
If NextHoliday <= LastHoliday Then
' First, check if NextHoliday hasn't been advanced.
If NextHoliday < LastHoliday Then
If Sgn(DateDiff("d", Date1, Holidays(NextHoliday))) = -Sign Then
' Weekend hasn't advanced NextHoliday.
NextHoliday = NextHoliday + 1
End If
End If
' Then, check if Date1 has reached a holiday.
If DateDiff("d", Date1, Holidays(NextHoliday)) = 0 Then
' This Date1 hits a holiday.
' Subtract one day to neutralize the one
' being added at the end of the loop.
Diff = Diff - Sign
' Adjust to the next holiday to check.
NextHoliday = NextHoliday + 1
End If
End If
End If
Diff = Diff + Sign
End Select
' Advance Date1.
Date1 = DateAdd("d", Sign, Date1)
Loop
End If
DateDiffWorkdays = Diff
End Function
' Returns the holidays between Date1 and Date2.
' The holidays are returned as an array with the
' dates ordered ascending, optionally descending.
'
' The array is declared static to speed up
' repeated calls with identical date parameters.
'
' Requires table Holiday with list of holidays.
'
' 2015-12-18. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function GetHolidays( _
ByVal Date1 As Date, _
ByVal Date2 As Date, _
Optional ByVal OrderDesc As Boolean) _
As Date()
' Constants for the arrays.
Const DimRecordCount As Long = 2
Const DimFieldOne As Long = 0
Static Date1Last As Date
Static Date2Last As Date
Static OrderLast As Boolean
Static DayRows As Variant
Static Days As Long
Dim rs As DAO.Recordset
' Cannot be declared Static.
Dim Holidays() As Date
If DateDiff("d", Date1, Date1Last) <> 0 Or _
DateDiff("d", Date2, Date2Last) <> 0 Or _
OrderDesc <> OrderLast Then
' Retrieve new range of holidays.
Set rs = DatesHoliday(Date1, Date2, OrderDesc)
' Save the current set of date parameters.
Date1Last = Date1
Date2Last = Date2
OrderLast = OrderDesc
Days = rs.RecordCount
If Days > 0 Then
' As repeated calls may happen, do a movefirst.
rs.MoveFirst
DayRows = rs.GetRows(Days)
' rs is now positioned at the last record.
End If
rs.Close
End If
If Days = 0 Then
' Leave Holidays() as an unassigned array.
Erase Holidays
Else
' Fill array to return.
ReDim Holidays(Days - 1)
For Days = LBound(DayRows, DimRecordCount) To UBound(DayRows, DimRecordCount)
Holidays(Days) = DayRows(DimFieldOne, Days)
Next
End If
Set rs = Nothing
GetHolidays = Holidays()
End Function
' Returns the holidays between Date1 and Date2.
' The holidays are returned as a recordset with the
' dates ordered ascending, optionally descending.
'
' Requires table Holiday with list of holidays.
'
' 2015-12-18. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function DatesHoliday( _
ByVal Date1 As Date, _
ByVal Date2 As Date, _
Optional ByVal ReverseOrder As Boolean) _
As DAO.Recordset
' The table that holds the holidays.
Const Table As String = "Holiday"
' The field of the table that holds the dates of the holidays.
Const Field As String = "Date"
Dim rs As DAO.Recordset
Dim SQL As String
Dim SqlDate1 As String
Dim SqlDate2 As String
Dim Order As String
SqlDate1 = Format(Date1, "\#yyyy\/mm\/dd\#")
SqlDate2 = Format(Date2, "\#yyyy\/mm\/dd\#")
ReverseOrder = ReverseOrder Xor (DateDiff("d", Date1, Date2) < 0)
Order = IIf(ReverseOrder, "Desc", "Asc")
SQL = "Select " & Field & " From " & Table & " " & _
"Where " & Field & " Between " & SqlDate1 & " And " & SqlDate2 & " " & _
"Order By 1 " & Order
Set rs = CurrentDb.OpenRecordset(SQL, dbOpenSnapshot)
Set DatesHoliday = rs
End Function
You'll see, that in its core it's nothing but a simple loop, which is so fast that attempts to optimise won't pay off for typical usage.
Maybe you try to use function networkdays
=NETWORKDAYS(start_date,end_date,holidays)
holidays is optional
For example, if you have the date January 4, 2016 (a Monday) in cell B4, and January 11, 2016 (also a Monday) in cell C4, this formula will return 6:
=NETWORKDAYS(B4,C4)
for VBA in ACCESS
Sub test()
Dim xl As Object
Set xl = CreateObject("Excel.Application")
BegDate = #4/11/2019#
EndDate = #6/11/2019#
result = xl.WorksheetFunction.NetworkDays(BegDate, EndDate) ' 44
Set xl = Nothing
End Sub
OR
this one
I would like the following code to return Monday date only if dateadd hits Saturday and Sunday. Unfortunately, now it returns Monday at all times even though dateadd doesn't fall on a Saturday or Sunday date.
Is it anyway to improve this code in order to return Monday only when needed?
Private Sub Worksheet_Change(ByVal target As Range)
Dim d1 As Date, d2 As Date, d3 As Date
d1 = NextMonday(Date, 1)
d2 = NextMonday(Date, 7)
d3 = NextMonday(Date, 5)
If Not Intersect(target, Range("H3:H150")) Is Nothing Then
If target.Value = 7 Then
target.Offset(0, 1).Value = d2
ElseIf target.Value = 5 Then
target.Offset(0, 1).Value = d3
ElseIf target.Value = 1 Then
target.Offset(0, 1).Value = d1
Else
End If
End If
End Sub
Function NextMonday(dtDate As Date, lngDaysToAdd As Long)
Dim intDaysOffset As Integer
NextMonday = DateAdd("d", lngDaysToAdd, dtDate)
intDaysOffset = (7 - Weekday(NextMonday, vbMonday)) + 1
NextMonday = DateAdd("d", intDaysOffset, NextMonday)
End Function
Having found your "landing date", you can use this generic function to return the Monday should the date be a weekend day:
Public Function DateSkipWeekend( _
ByVal datDate As Date, _
Optional ByVal booReverse As Boolean) _
As Date
' Purpose: Calculate first working day equal to or following/preceding datDate.
' Assumes: 5 or 6 working days per week. Weekend is (Saturday and) Sunday.
' Limitation: Does not count for public holidays.
'
' May be freely used and distributed.
' 1999-07-03, Gustav Brock, Cactus Data ApS, Copenhagen
Const cintWorkdaysOfWeek As Integer = 5
Dim bytSunday As Byte
Dim bytWeekday As Byte
bytSunday = Weekday(vbSunday, vbMonday)
bytWeekday = Weekday(datDate, vbMonday)
If bytWeekday > cintWorkdaysOfWeek Then
' Weekend.
If booReverse = False Then
' Get following workday.
datDate = DateAdd("d", 1 + bytSunday - bytWeekday, datDate)
Else
' Get preceding workday.
datDate = DateAdd("d", cintWorkdaysOfWeek - bytWeekday, datDate)
End If
End If
DateSkipWeekend = datDate
End Function
And in your function:
Function NextMonday(dtDate As Date, lngDaysToAdd As Long)
NextMonday = DateSkipWeekend(DateAdd("d", lngDaysToAdd, dtDate))
End Function
Check the resulting day and set the return value accordingly. But of course, your function should be renamed, I suggest NextWorkday because you want to skip only weekend days.
Function NextWorkday(dtDate As Date, lngDaysToAdd As Long)
NextWorkday = dtDate + lngDaysToAdd
Select Case Weekday(NextWorkday, vbMonday)
Case 6: NextWorkday = NextWorkday + 2
Case 7: NextWorkday = NextWorkday + 1
End Select
End Function
I'm not sure why you are updating only for some values, but if you still want this checking, your code can be translated to this:
Private Sub Worksheet_Change(ByVal Target As Range)
If Target.Column <> Columns("H").Column Then Exit Sub
Select Case Target.Value2
Case 1, 5, 7: Target.Offset(0, 1).value = NextWorkday(Date, Target.Value2)
End Select
End Sub
You could use the built-in Excel function Workday:
Function NextMonday(dtDate As Date, lngDaysToAdd As Long) As Date
NextMonday = Application.WorkDay(dtDate + lngDaysToAdd - 1, 1)
End Function
The subtraction of 1 from the future date is to allow one workday to be added. (Adding zero workdays to a date that is a Saturday/Sunday would return the Saturday/Sunday.)
(And, as A.S.H suggested, it would be a good idea to rename that Function as it doesn't return "next Monday".)
However, if I am reading your VBA code correctly, you may be able to just use WORKDAY in an Excel cell, e.g. in cell I3 use
=WORKDAY(TODAY()+H3-1,1)