DateAddWorkdays giving inconsistent results - vba

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

Related

Find end date given startdate in MS ACCESS

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

How to group dates into weeks?

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

Difference between two dates in number of months and days SQL

I'm trying to display the difference between two dates in order to calculate the monthly rent, I'm expecting the exact number of months and days because the contract is "monthly" so if it is February, March or April, the monthly amount is fixed.
so let's say:
Strat_D = 05-Aug-2020
End_D = 20-Sept-2020
I'm using this code to get number of days:
DateDiff("d",[Start_D],[End_D])
the output is: 45 , but expecting: 1 month & 15 days OR 1.5
and if I use:
DateDiff("m",[Start_D],[End_D])
output is: 1 , but expecting: 1.5
Thanks in advance
As the day count of months varies, you have to count by days to get as close as possible, as there never (except within a month or within July/August or December/January) will be an exact value:
' Returns the decimal count of months between Date1 and Date2.
'
' Rounds by default to two decimals, as more decimals has no meaning
' due to the varying count of days of a month.
' Optionally, don't round, by setting Round2 to False.
'
' 2017-01-24. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function TotalMonths( _
ByVal Date1 As Date, _
ByVal Date2 As Date, _
Optional Round2 As Boolean = True) _
As Double
Dim Months As Double
Dim Part1 As Double
Dim Part2 As Double
Dim Fraction As Double
Dim Result As Double
Months = DateDiff("m", Date1, Date2)
Part1 = (Day(Date1) - 1) / DaysInMonth(Date1)
Part2 = (Day(Date2) - 1) / DaysInMonth(Date2)
If Round2 = True Then
' Round to two decimals.
Fraction = (-Part1 + Part2) * 100
Result = Months + Int(Fraction + 0.5) / 100
Else
Result = Months - Part1 + Part2
End If
TotalMonths = Result
End Function
' Returns the count of days of the month of Date1.
'
' 2016-02-14. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function DaysInMonth( _
ByVal Date1 As Date) _
As Integer
Const MaxDateValue As Date = #12/31/9999#
Const MaxDayValue As Integer = 31
Dim Days As Integer
If DateDiff("m", Date1, MaxDateValue) = 0 Then
Days = MaxDayValue
Else
Days = Day(DateSerial(Year(Date1), Month(Date1) + 1, 0))
End If
DaysInMonth = Days
End Function
Results:
? TotalMonths(#2020/08/05#, #2020/09/20#)
1.5
? TotalMonths(#2020/11/15#, #2021/02/15#)
3.03

MS Access query, how to use SQL to group single dates into weeks

I currently have two tables. One has Employee Names and a number associated to that name.
The other date has time sheet date with columns for the employee number, the date and the number of hours worked on that date.
I want to create a cross tab query that shows the employee names in one column with the date for the end of the week in each column, then show to sum of hours for that week for a particular employee.
My current query works but only groups by month. I am struggle to work out how to group days into weeks.
TRANSFORM Sum(tblTimeSheetData.WorkHours) AS SumOfHours
SELECT tblEmployees.Combined
FROM tblTimeSheetData RIGHT JOIN tblEmployees ON tblTimeSheetData.EmployeeID =
tblEmployees.EmployeeID
GROUP BY tblEmployees.Combined
ORDER BY tblEmployees.Combined, Format([WorkDate],"yyyy-mm")
PIVOT Format([WorkDate],"yyyy-mm");
As the first and last week numbers cross calendar years, both year and week number must be included:
Option Compare Database
Option Explicit
Public Const MaxWeekValue As Integer = 53
Public Const MinWeekValue As Integer = 1
Public Const MaxMonthValue As Integer = 12
Public Const MinMonthValue As Integer = 1
' 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 WeekSeparator 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(WeekSeparator, 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
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
If you just want to pivot on the ultimo week date, you can use this expression:
DateAdd("d", 7 - Weekday([WorkDate], vbMonday), [WorkDate])
This assumes ISO week numbering where Monday is the first day of the week.
You can try getting the week with datepart function like
SELECT DATEPART(ww, 'your date') AS week;
and then group by week.
Here more info
https://www.w3schools.com/sql/func_sqlserver_datepart.asp

Why does my CalcWorkingDays VBA Function give me two different results on the same period?

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