Rank Table by Group in MS Access SQL [duplicate] - sql

I need to write a rank query, but i have to do it on access.
How do I translate the following query?
PERIODO,
SUM(IMP_ENTRATE_ATT) AS TOT_ENTRATE,
RANK89 OVER(PARTITION BY PERIODO ORDER BY TOT_ENTRATE DESC)AS RANK
FROM ENTRATE
ORDER BY RANK ```
e.i.
COMPANY | MONTH | REVENUES
'''
a | 01 | 100
b | 02 | 55
a | 02 | 230
c | 03 | 25
a | 01 | 70
b | 02 | 100
'''
I need to find the sum(revenues) per month per company

Access SQL has no ranking functions. You can use my function RowRank from GitHub: VBA.RowNumbers:
' Returns, by the value of a field, the rank of one or more records of a table or query.
' Supports all five common ranking strategies (methods).
'
' Source:
' WikiPedia: https://en.wikipedia.org/wiki/Ranking
'
' Supports ranking of descending as well as ascending values.
' Any ranking will require one table scan only.
' For strategy Ordinal, a a second field with a subvalue must be used.
'
' Typical usage (table Products of Northwind sample database):
'
' SELECT Products.*, RowRank("[Standard Cost]","[Products]",[Standard Cost]) AS Rank
' FROM Products
' ORDER BY Products.[Standard Cost] DESC;
'
' Typical usage for strategy Ordinal with a second field ([Product Code]) holding the subvalues:
'
' SELECT Products.*, RowRank("[Standard Cost],[Product Code]","[Products]",[Standard Cost],[Product Code],2) AS Ordinal
' FROM Products
' ORDER BY Products.[Standard Cost] DESC;
'
' To obtain a rank, the first three parameters must be passed.
' Four parameters is required for strategy Ordinal to be returned properly.
' The remaining parameters are optional.
'
' The ranking will be cached until Order is changed or RowRank is called to clear the cache.
' To clear the cache, call RowRank with no parameters:
'
' RowRank
'
' Parameters:
'
' Expression: One field name for other strategies than Ordinal, two field names for this.
' Domain: Table or query name.
' Value: The values to rank.
' SubValue: The subvalues to rank when using strategy Ordinal.
' Strategy: Strategy for the ranking.
' Order: The order by which to rank the values (and subvalues).
'
' 2019-07-11. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function RowRank( _
Optional ByVal Expression As String, _
Optional ByVal Domain As String, _
Optional ByVal Value As Variant, _
Optional ByVal SubValue As Variant, _
Optional ByVal Strategy As ApRankingStrategy = ApRankingStrategy.apStandardCompetition, _
Optional ByVal Order As ApRankingOrder = ApRankingOrder.apDescending) _
As Double
Const SqlMask1 As String = "Select Top 1 {0} From {1}"
Const SqlMask As String = "Select {0} From {1} Order By 1 {2}"
Const SqlOrder As String = ",{0} {1}"
Const OrderAsc As String = "Asc"
Const OrderDesc As String = "Desc"
Const FirstStrategy As Integer = ApRankingStrategy.apDense
Const LastStrategy As Integer = ApRankingStrategy.apFractional
' Expected error codes to accept.
Const CannotAddKey As Long = 457
Const CannotFindKey As Long = 5
' Uncommon character string to assemble Key and SubKey as a compound key.
Const KeySeparator As String = "¤§¤"
' Array of the collections for the five strategies.
Static Ranks(FirstStrategy To LastStrategy) As Collection
' The last sort order used.
Static LastOrder As ApRankingOrder
Dim Records As DAO.Recordset
' Array to hold the rank for each strategy.
Dim Rank(FirstStrategy To LastStrategy) As Double
Dim Item As Integer
Dim Sql As String
Dim SortCount As Integer
Dim SortOrder As String
Dim LastKey As String
Dim Key As String
Dim SubKey As String
Dim Dupes As Integer
Dim Delta As Long
Dim ThisStrategy As ApRankingStrategy
On Error GoTo Err_RowRank
If Expression = "" Then
' Erase the collections of keys.
For Item = LBound(Ranks) To UBound(Ranks)
Set Ranks(Item) = Nothing
Next
Else
If LastOrder <> Order Or Ranks(FirstStrategy) Is Nothing Then
' Initialize the collections and reset their ranks.
For Item = LBound(Ranks) To UBound(Ranks)
Set Ranks(Item) = New Collection
Rank(Item) = 0
Next
' Build order clause.
Sql = Replace(Replace(SqlMask1, "{0}", Expression), "{1}", Domain)
SortCount = CurrentDb.OpenRecordset(Sql, dbReadOnly).Fields.Count
If Order = ApRankingOrder.apDescending Then
' Descending sorting (default).
SortOrder = OrderDesc
Else
' Ascending sorting.
SortOrder = OrderAsc
End If
LastOrder = Order
' Build SQL.
Sql = Replace(Replace(Replace(SqlMask, "{0}", Expression), "{1}", Domain), "{2}", SortOrder)
' Add a second sort field, if present.
If SortCount >= 2 Then
Sql = Sql & Replace(Replace(SqlOrder, "{0}", 2), "{1}", SortOrder)
End If
' Open ordered recordset.
Set Records = CurrentDb.OpenRecordset(Sql, dbReadOnly)
' Loop the recordset once while creating all the collections of ranks.
While Not Records.EOF
Key = CStr(Nz(Records.Fields(0).Value))
SubKey = ""
' Create the sub key if a second field is present.
If SortCount > 1 Then
SubKey = CStr(Nz(Records.Fields(1).Value))
End If
If LastKey <> Key Then
' Add new entries.
For ThisStrategy = FirstStrategy To LastStrategy
Select Case ThisStrategy
Case ApRankingStrategy.apDense
Rank(ThisStrategy) = Rank(ThisStrategy) + 1
Case ApRankingStrategy.apStandardCompetition
Rank(ThisStrategy) = Rank(ThisStrategy) + 1 + Dupes
Dupes = 0
Case ApRankingStrategy.apModifiedCompetition
Rank(ThisStrategy) = Rank(ThisStrategy) + 1
Case ApRankingStrategy.apOrdinal
Rank(ThisStrategy) = Rank(ThisStrategy) + 1
' Add entry using both Key and SubKey
Ranks(ThisStrategy).Add Rank(ThisStrategy), Key & KeySeparator & SubKey
Case ApRankingStrategy.apFractional
Rank(ThisStrategy) = Rank(ThisStrategy) + 1 + Delta / 2
Delta = 0
End Select
If ThisStrategy = ApRankingStrategy.apOrdinal Then
' Key with SubKey has been added above for this strategy.
Else
' Add key for all other strategies.
Ranks(ThisStrategy).Add Rank(ThisStrategy), Key
End If
Next
LastKey = Key
Else
' Modify entries and/or counters for those strategies that require this for a repeated key.
For ThisStrategy = FirstStrategy To LastStrategy
Select Case ThisStrategy
Case ApRankingStrategy.apDense
Case ApRankingStrategy.apStandardCompetition
Dupes = Dupes + 1
Case ApRankingStrategy.apModifiedCompetition
Rank(ThisStrategy) = Rank(ThisStrategy) + 1
Ranks(ThisStrategy).Remove Key
Ranks(ThisStrategy).Add Rank(ThisStrategy), Key
Case ApRankingStrategy.apOrdinal
Rank(ThisStrategy) = Rank(ThisStrategy) + 1
' Will fail for a repeated value of SubKey.
Ranks(ThisStrategy).Add Rank(ThisStrategy), Key & KeySeparator & SubKey
Case ApRankingStrategy.apFractional
Rank(ThisStrategy) = Rank(ThisStrategy) + 0.5
Ranks(ThisStrategy).Remove Key
Ranks(ThisStrategy).Add Rank(ThisStrategy), Key
Delta = Delta + 1
End Select
Next
End If
Records.MoveNext
Wend
Records.Close
End If
' Retrieve the rank for the current strategy.
If Strategy = ApRankingStrategy.apOrdinal Then
' Use both Value and SubValue.
Key = CStr(Nz(Value)) & KeySeparator & CStr(Nz(SubValue))
Else
' Use Value only.
Key = CStr(Nz(Value))
End If
' Will fail if key isn't present.
Rank(Strategy) = Ranks(Strategy).Item(Key)
End If
RowRank = Rank(Strategy)
Exit_RowRank:
Exit Function
Err_RowRank:
Select Case Err
Case CannotAddKey
' Key is present, thus cannot be added again.
Resume Next
Case CannotFindKey
' Key is not present, thus cannot be removed.
Resume Next
Case Else
' Some other error. Ignore.
Resume Exit_RowRank
End Select
End Function

Related

DateAddWorkdays giving inconsistent results

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

How to generate column with row number?

I'm trying to make an query that will generate column with unique row number generated incrementally. My table with data don't have any unique value.
I was trying to get Gustav script, but I only manage to get it return 1 in every row.
Public Function RowCounter( _
ByVal strKey As String, _
ByVal booReset As Boolean, _
Optional ByVal strGroupKey As String) _
As Long
' Builds consecutive RowIDs in select, append or create query
' with the possibility of automatic reset.
' Optionally a grouping key can be passed to reset the row count
' for every group key.
'
' Usage (typical select query):
' SELECT RowCounter(CStr([ID]),False) AS RowID, *
' FROM tblSomeTable
' WHERE (RowCounter(CStr([ID]),False) <> RowCounter("",True));
'
' Usage (with group key):
' SELECT RowCounter(CStr([ID]),False,CStr[GroupID])) AS RowID, *
' FROM tblSomeTable
' WHERE (RowCounter(CStr([ID]),False) <> RowCounter("",True));
'
' The Where statement resets the counter when the query is run
' and is needed for browsing a select query.
'
' Usage (typical append query, manual reset):
' 1. Reset counter manually:
' Call RowCounter(vbNullString, False)
' 2. Run query:
' INSERT INTO tblTemp ( RowID )
' SELECT RowCounter(CStr([ID]),False) AS RowID, *
' FROM tblSomeTable;
'
' Usage (typical append query, automatic reset):
' INSERT INTO tblTemp ( RowID )
' SELECT RowCounter(CStr([ID]),False) AS RowID, *
' FROM tblSomeTable
' WHERE (RowCounter("",True)=0);
'
' 2002-04-13. Cactus Data ApS. CPH
' 2002-09-09. Str() sometimes fails. Replaced with CStr().
' 2005-10-21. Str(col.Count + 1) reduced to col.Count + 1.
' 2008-02-27. Optional group parameter added.
' 2010-08-04. Corrected that group key missed first row in group.
Static col As New Collection
Static strGroup As String
On Error GoTo Err_RowCounter
If booReset = True Then
Set col = Nothing
ElseIf strGroup <> strGroupKey Then
Set col = Nothing
strGroup = strGroupKey
col.Add 1, strKey
Else
col.Add col.Count + 1, strKey
End If
RowCounter = col(strKey)
Exit_RowCounter:
Exit Function
Err_RowCounter:
Select Case Err
Case 457
' Key is present.
Resume Next
Case Else
' Some other error.
Resume Exit_RowCounter
End Select
End Function
Sql query that I'm trying to get working is like this:
SELECT RowCounter("Query1",False) AS RowID, *
FROM Query1
WHERE (RowCounter("Query1",False)<>RowCounter("",True));
What am I missing to get this to work?
You are passing a fixed string as the parameter - and this gets No. 1.
Use the ID of the records of the query, like:
SELECT RowCounter(CStr([ID]),False) AS RowID, *

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

Error "object required" in VBA , referred to duplicate questions

My purpose is to split a task into constituent tasks and find the most important one.The macro is written in "May" sheet of workallotment.xlsm and the tasks are in tasks.xlsx
For example:
Constituents Constituents Important Imp
Praveen T1 T2 T3 T4 T5 T6 T1+T2+T3 =T5 T3+T5+T6 =T9 T1 T6
4 3 1 2 8 9
Karthik P1 P2 P3 P4 " among T1,T2,T3- T1 takes more time".its imp
6 3 2 2
Walter c1 c2 c3 c4
1 2 3 4
Arvind g1 g2 g3
2 1 3
Sreelatha h1 h2 h3
2 1 1
Code:
Sub workallotment()
Dim workallotmentWB, tasksWB As Workbook
Dim waSheet As Worksheet
Dim str(9) As String
Dim splitArray() As String, S(10) As String
Dim col_new As Integer
Dim wa_nameRng As Range
Dim r As Integer, max As Integer, imps As String
Dim wa_nameRow, wa_firstRow, wa_lastRow As Integer 'work allotment rows
Dim t_firstRow, t_lastrow As Integer 'task rows
Dim curTaskCol As Integer 'current task column
Dim wa_tmpcol As Integer 'work allotment, temp column
Set workallotmentWB = ThisWorkbook
Set tasksWB = Workbooks.Open("E:/tasks.xlsx")
'notes on data structure:
'- tasks workbook:
'first name starts in A1 of "Sheet1"
'- workallotment workbook:
'first name starts in A2 of Sheet named "workallotment"
'tasks are to be written starting in B2
'in Row 1 are headers (number of days)
t_firstRow = 1
wa_firstRow = 2
wa_nameRow = 0
Set waSheet = workallotmentWB.Worksheets("May") ' in this file - workallotment.xlsm
With tasksWB.Worksheets("May") ' in tasks.xlsx which is attached
'finding the last rows
t_lastrow = .Range("A1000000").End(xlUp).row + 1
wa_lastRow = waSheet.Range("A1000000").End(xlUp).row
'goes through all the names in tasks_Sheet1
For r = t_firstRow To t_lastrow Step 2
Set wa_nameRng = waSheet.Range("A:A").find(.Range("A" & r).Value, _
LookIn:=xlValues, LookAt:=xlWhole, SearchDirection:=xlNext, MatchCase:=False)
If Not wa_nameRng Is Nothing Then
wa_nameRow = wa_nameRng.row
curTaskCol = 2
wa_tmpcol = 2
Do While Not IsEmpty(.Cells(r, curTaskCol).Value)
For C = 1 To .Cells(r + 1, curTaskCol).Value
waSheet.Cells(wa_nameRow, wa_tmpcol).Value = .Cells(r, curTaskCol).Value
wa_tmpcol = wa_tmpcol + 1
Next C
curTaskCol = curTaskCol + 1
Loop
End If
Next r
End With
MsgBox ("done")
For r = t_firstRow To t_lastrow Step 2 ' loop to find importance
col = 2 'setting to initial col
curTaskCol = 17 ' position input - constituent jobs at 17th col in tasks.xls
Do While Not IsEmpty(tasksWB.Worksheets("May").Cells(r, curTaskCol).Value)
str(curTaskCol - 16) = tasksWB.Worksheets("May").Cells(r, curTaskCol).Value
' reading input to first array of string element
substr = Left(str(curTaskCol - 16), Application.WorksheetFunction.find("=", str(curTaskCol - 16)) - 1) ' if T1+T2=T3 it'll look before "=" symbol
MsgBox (substr)
splitArray() = Split(substr, "+") ' if T1+T2 it will be split as T1 & T2
For i = LBound(splitArray) To UBound(splitArray)
S(i + 1) = splitArray(i) ' assigning split elements to string array
Next i
For i = LBound(splitArray) To UBound(splitArray)
col_new = 2 ' checking from 2nd column
Do While Not IsEmpty(tasksWB.Worksheets("May").Cells(r, col_new).Value)
If (S(i + 1) = tasksWB.Worksheets("May").Cells(r, col_new).Value) Then 'initialising max and imps
imps = S(i + 1) ' most important job
max = tasksWB.Worksheets("May").Cells(r + 1, col_new).Value
End If ' maximum time taken for task
col_new = col_new + 1
Loop
For j = LBound(splitArray) To UBound(splitArray)
col_new = findcol(S(j + 1), r, tasksWB)
If (max < tasksWB.Worksheets("May").Cells(r + 1, col_new).Value) Then
max = tasksWB.Worksheets("May").Cells(r + 1, col_new).Value
imps = tasksWB.Worksheets("May").Cells(r, col_new).Value
End If
Next j
Next i
tasksWB.Worksheets("May").Cells(r, curTaskCol + 6).Value = imps
' assign most IMPORTANT task on 6th column from current column
curTaskCol = curTaskCol + 1 ' RUNTIME ERROR 1004
Loop
Next r
End Sub
Public Function findcol(S As String, row As Integer, theWB As Workbook) As Integer
Dim col As Integer, addr As Integer
col = 2 ' checking from column 2
'Set tasksWB = Workbooks.Open("E:/tasks.xlsx")
Do While Not IsEmpty(theWB.Worksheets("May").Cells(row, col).Value)
If (StrComp(Trim(S), Trim(theWB.Worksheets("May").Cells(row, col).Value)) = 0) Then
addr = col ' if task string is found in column
End If
col = col + 1 ' return column found
Loop
findcol = addr
End Function
Krishnan,
In your main proc workallotment you declare the variable tasksWB.
In your method 'findcol' you then reference tasksWB. It looks like you've pulled this code out of the main proc. The tasksWB only has scope within workallot and so you need to give findcol this object so it will have it within it's scope as well.
I would recommend that you pass the tasksWB into the method, as a third parameter.
Your method would then look as follows.
Edit for your comment of why findcol doesn't return. The Exit Function will ensure that the method is exited immediately after setting the return value. Without this you would end up in the asking for the correct task name again.
Public Function findcol(S As String, row As Integer, theWB as Workbook) As Integer
col = 2 ' checking from column 2
Do While Not IsEmpty(theWB.Worksheets("May").Cells(row, col).Value)
If (S = theWB.Worksheets("May").Cells(row, col).Value) Then
findcol = col ' if task string is found in column
Exit Function
End If
'MsgBox ("Enter correct task names") Not sure why this is here.
col = col + 1 ' return column found
Loop
End Function
and you'd call it with
col_new = findcol(S(j + 1), r, tasksWB) ' ERROR line function to find column of task string
This will ensure that you do not "leak" your variable definition into global scope, and that you also ensure that your method doesn't depend on external globals.
Edit 3:
Your findcol is still wrong.
Public Function findcol(S As String, row As Integer, theWB As Workbook) As Integer
Dim col As Integer
'******* you don't need this because you can exit early
'Dim addr As Integer
col = 2 ' checking from column 2
'***** THIS LINE NEEDS TO BE REMOVED because you are using theWB being passed in *****
'Set tasksWB = Workbooks.Open("E:/tasks.xlsx")
Do While Not IsEmpty(theWB.Worksheets("May").Cells(row, col).Value)
'****** this line must use theWB
'If (StrComp(Trim(S), Trim(tasksWB.Worksheets("May").Cells(row, col).Value)) = 0) Then
If (StrComp(Trim(S), Trim(theWB.Worksheets("May").Cells(row, col).Value)) = 0) Then
'************* you can exit early once you've found what you need.
'addr = col ' if task string is found in column
findcol = col
exit function
End If
col = col + 1 ' return column found
Loop
' You can exit early so don't need this.
' findcol = addr
End Function
You should probably do a check when you call the function that the value hasn't returned 0, eg
new_col = findcol( .... )
if new_col = 0 then
msgbox "couldn't find the column with that str" & S(j + 1)
end if
tasksWB isn't recognized in the findcol function as it is declared as Private (=Dim) in the main process.
Declare it at the top of your module, and it'll work! ;)

Updating A DataGridView Cell Incrementally

I'm currently having a slight issue duplicating a row and incrementing the sequence number.
So based on a button click, this is how I'm duplicating row 0, duplicated only one time per click.
Dim dr As DataRow
For n As Integer = 0 To 0 ' how many dupes you want
dr = tbl.NewRow
For c As Integer = 0 To tbl.Columns.Count - 1 ' copy data from 0 to NewRow
dr.Item(c) = tbl.Rows(0).Item(c)
Next
tbl.Rows.Add(dr) ' add NewRow to datatable
Next n
Here's how I'm creating the sequence number, pads with leading zeros, which seems to increment, but only after I click the duplicate button, so essentially the last row added, it the duplicated row 0, but doesn't represent the new sequence number needed.
'UPDATE SEQUENCE NUMBER
i += 1
Dim value As Integer = i
Dim r As Integer
Dim decimalLength1 As Integer = value.ToString("D").Length + 7
Dim decimalLength2 As Integer = value.ToString("D").Length + 6
Dim decimalLength3 As Integer = value.ToString("D").Length + 5
Dim decimalLength4 As Integer = value.ToString("D").Length + 4
If i >= 0 And i <= 9 Then
'1 TO 9 FORMAT
DataGridView1.CurrentCell = DataGridView1.CurrentRow.Cells("sequence")
DataGridView1.Item(73, r).Value = value.ToString("D" + decimalLength1.ToString())
'Debug.Print(value.ToString("D" + decimalLength1.ToString()))
ElseIf i >= 10 And i <= 99 Then
'10 TO 99 FORMAT
DataGridView1.CurrentCell = DataGridView1.CurrentRow.Cells("sequence")
DataGridView1.Item(73, r).Value = value.ToString("D" + decimalLength2.ToString())
'Debug.Print(value.ToString("D" + decimalLength1.ToString()))
ElseIf i >= 100 And i <= 999 Then
'100 TO 999 FORMAT
DataGridView1.CurrentCell = DataGridView1.CurrentRow.Cells("sequence")
DataGridView1.Item(73, r).Value = value.ToString("D" + decimalLength3.ToString())
'Debug.Print(value.ToString("D" + decimalLength1.ToString()))
ElseIf i >= 1000 And i <= 9999 Then
'1000 TO 9999 FORMAT
DataGridView1.CurrentCell = DataGridView1.CurrentRow.Cells("sequence")
DataGridView1.Item(73, r).Value = value.ToString("D" + decimalLength4.ToString())
'Debug.Print(value.ToString("D" + decimalLength1.ToString()))
End If
Row 0 will always have a sequence number of 1, so in theory I need to start incrementing at 2.
Suggestions? Is there a better/cleaner way of doing this?
UPDATE 2
Dim startSeq As Integer = Convert.ToInt32(tbl.Rows(0).Item(73))
MsgBox("startSeq = " & startSeq)
For n As Integer = 0 To NumericUpDown1.Value - 1
MsgBox("n = " & n)
dr = tbl.NewRow
For c As Integer = 0 To tbl.Columns.Count - 1
dr.Item(c) = tbl.Rows(0).Item(c)
If c = "73" Then ' if this is the SEQ column,
' add the current seq val to the seq column
dr.Item(c) = (startSeq + n).ToString("00000000")
End If
Next c
tbl.Rows.Add(dr)
Next n
It seems like you should be able to add the sequencer as you create the duplicates. Perhaps make it a method and pass the index of the column which has the sequence string. Something like:
Private Sub DuplicateRows(ColIndx As Integer,
Dupes As Integer)
' start value is Row(0) + 1
Dim startSeq As Integer = Convert.ToInt32(tbl.Rows(0).Item(ColIndx )) + 1
For n As Integer = 0 to Dupes -1
dr = tbl.NewRow
For c As Integer = 0 To tbl.Columns.Count - 1
If c = ColIndx Then ' if this is the SEQ column,
' add the current seq val to the seq column
dr.Item(c) = (startSeq + n).ToString("00000000")
Else
' otherwise copy the data from Row(0)
dr.Item(c) = tbl.Rows(0).Item(c)
End If
Next c
tbl.Rows.Add(dr)
Next n
End Sub
This should initialize each new row with an incremented counter. Is there a better/cleaner way of doing this
a) you should be adding to the DataTable, not the DGV if it is bound
b) (startSeq + n).ToString("00000000") should work to do the padding etc instead of that ugly block of code.
c) Use Option Strict On. If c = "73" ... is nonsense which makes the compiler guess at your intentions. Its is bug food.
d) Hardcoding "73" may work this time, but previously you said it could be anywhere. The code below finds the sequence column based on the name so it can appear anywhere. Rather than a form level var, you could find it just before you make the dupes or even in the Dupes procedure.
e) Dim startSeq As Integer = Convert.ToInt32(tbl.Rows(0).Item(73)) if you examine the answer above, this should be ... + 1 to increment the first value.
Usage:
Private tbl As DataTable ' table loaded from flat file
Private SeqColIndex As Integer ' assigned somewhere to
' point to the "sequence" column
' method to load data
Dim connstr = "Provider=Microsoft.ACE.OLEDB.12.0;..."
Using cn As New OleDbConnection(connstr)
...
End Using
' FIND the sequence column for this session
For n = 0 To tbl.Columns.Count - 1
If tbl.Columns(n).ColumnName.ToLowerInvariant = "sequence" Then
SeqColIndex = n
Exit For
End If
Next
' later to add some rows
Private Sub ButtonAddRow_Click(...
DuplicateRows(SeqColIndex, NumericUpDown1.Value)
End Sub