Hello all. I am trying to speed up some code that I have written. What I have works perfectly fine, but is quite slow. I would like to see if there is a way to increase the speed.
My workbook consists of 3 sheets. The first tab (named "Access Data") contains a table (named "tblData") with 5 fields. The headers in sequential order are "User", "Dept", "Module", "Header", and "Job Name". The 2 relevant fields are the "User" and "Job Name" fields. The "User" field contains a series of user names. The "Job Name" field contains all the different jobs that they are able to perform. The table is sorted first by "User", then by "Job Name", both in ascending order. Each User can have multiple jobs, but a given job can only appear once for a specific user. However, a given job may appear for more than 1 user.
The 2nd sheet (named "SOD Testing") is just a repository for the results of the code execution. Cell B1 contains a simple title string ("List of Duties/Jobs"). Cell B2 contains the formula
Cell C1 contains the formula
This results in sorted lists of the unique Job Names. The rest of this sheet is filled out by VBA.
The 3rd sheet (named "SOD Notifications") is used to hold a list of the users and the jobs that they perform. However, only certain users and jobs qualify to be on this list (I'll explain more later). Range A1:C1 contains some simple headers ("User", "Job Name 1", "Job Name 2") and the rest of the sheet is blank, but is filled out by VBA.
My goal is 2-fold. First, based on the grid established on "SOD Testing", I want to know how many users are able to do each job listed there. For example, if cell B4 contains "Checks and Registers" and cell E1 contains "Manual Checks", then cell E4 should contain the number of users who are able to do both "Checks and Registers" and "Manual Checks". If there are 3 users that are able to do both jobs, then cell E4 should contain the number 3. Second, certain jobs are not allowed to be performed by the same user. On the "SOD Testing" sheet, the intersecting cell of these jobs is highlighted in red. For example, if cell B10 contains "Void Checks" and cell F1 contains "Prepare Check Proofing" and these two jobs are not allowed to be performed by the same user, then cell F10 would be highlighted in red. This is done manually and is done before running any code.
If my code detects that there is a conflict (in other words, if it identifies that the same user is allowed to perform two conflicting jobs), then it lists the User and both Job Names on the "SOD Notifications" tab starting in cell A2.
Currently, my tblData contains almost 2,000 records, and there are almost 300 unique Job Names. This is a smaller sampling size, and will get larger when we use this for other scenarios. As a result, I'm hoping to find a way to run this code quickly. I have come up with 2 different solutions. Both of these work perfectly. However, they are both fairly slow. In order to speed up the process, I am hoping that somebody can provide some insight into speeding this up. Thank you very much for any and all assistance!
Edit: I'm reasonably comfortable with Power Query and Power Pivot, so solutions involving them are perfectly fine as well.
Note: Both code segments are called while the "SOD Testing" sheet is the active sheet.
Version 1 (no arrays)
Version 2 (with arrays)
My workbook consists of 3 sheets. The first tab (named "Access Data") contains a table (named "tblData") with 5 fields. The headers in sequential order are "User", "Dept", "Module", "Header", and "Job Name". The 2 relevant fields are the "User" and "Job Name" fields. The "User" field contains a series of user names. The "Job Name" field contains all the different jobs that they are able to perform. The table is sorted first by "User", then by "Job Name", both in ascending order. Each User can have multiple jobs, but a given job can only appear once for a specific user. However, a given job may appear for more than 1 user.
The 2nd sheet (named "SOD Testing") is just a repository for the results of the code execution. Cell B1 contains a simple title string ("List of Duties/Jobs"). Cell B2 contains the formula
Excel Formula:
=SORT(UNIQUE(tblData[Job Name]))
Excel Formula:
=TRANSPOSE(SORT(UNIQUE(tblData[Job Name])))
The 3rd sheet (named "SOD Notifications") is used to hold a list of the users and the jobs that they perform. However, only certain users and jobs qualify to be on this list (I'll explain more later). Range A1:C1 contains some simple headers ("User", "Job Name 1", "Job Name 2") and the rest of the sheet is blank, but is filled out by VBA.
My goal is 2-fold. First, based on the grid established on "SOD Testing", I want to know how many users are able to do each job listed there. For example, if cell B4 contains "Checks and Registers" and cell E1 contains "Manual Checks", then cell E4 should contain the number of users who are able to do both "Checks and Registers" and "Manual Checks". If there are 3 users that are able to do both jobs, then cell E4 should contain the number 3. Second, certain jobs are not allowed to be performed by the same user. On the "SOD Testing" sheet, the intersecting cell of these jobs is highlighted in red. For example, if cell B10 contains "Void Checks" and cell F1 contains "Prepare Check Proofing" and these two jobs are not allowed to be performed by the same user, then cell F10 would be highlighted in red. This is done manually and is done before running any code.
If my code detects that there is a conflict (in other words, if it identifies that the same user is allowed to perform two conflicting jobs), then it lists the User and both Job Names on the "SOD Notifications" tab starting in cell A2.
Currently, my tblData contains almost 2,000 records, and there are almost 300 unique Job Names. This is a smaller sampling size, and will get larger when we use this for other scenarios. As a result, I'm hoping to find a way to run this code quickly. I have come up with 2 different solutions. Both of these work perfectly. However, they are both fairly slow. In order to speed up the process, I am hoping that somebody can provide some insight into speeding this up. Thank you very much for any and all assistance!
Edit: I'm reasonably comfortable with Power Query and Power Pivot, so solutions involving them are perfectly fine as well.
Note: Both code segments are called while the "SOD Testing" sheet is the active sheet.
Version 1 (no arrays)
VBA Code:
Sub Check_Duties()
Dim wsData As Worksheet, strFirstAddress As String
Dim r As Range, strJob As String, rngJob As Range, strUser As String, intOccurrences As Integer
Dim intTotal As Integer, wsNotifications As Worksheet, rngDuties As Range, msgContinue As VbMsgBoxResult
Dim loData As ListObject, lngFoundRow As Long
msgContinue = MsgBox(Prompt:="CAUTION!! This process can take several minutes to complete. " & _
"During this time, you will not be able to use Excel. " & _
"Are you sure you wish to continue?", _
Buttons:=vbYesNo + vbExclamation + vbDefaultButton2, _
Title:="Extended Processing Time Required")
If msgContinue = vbNo Then Exit Sub
With Application
.ScreenUpdating = False
.Calculation = xlCalculationManual
End With
'Set the major variables that will be used throughout the code
Set wsData = Sheets("Access Data")
Set wsNotifications = Sheets("SOD Notifications")
Set loData = wsData.ListObjects(1)
Set rngDuties = Range(Cells(2, "C"), Cells(Cells(Rows.Count, "B").End(xlUp).Row, Cells(1, Columns.Count).End(xlToLeft).Column))
'Clear any existing data to start fresh. Note that cell fill colors are intentionally left intact.
wsNotifications.Range("A2:C" & wsNotifications.Cells(Rows.Count, "A").End(xlUp).Offset(1).Row).ClearContents
rngDuties.ClearContents
For Each r In rngDuties
'Make sure that the row and column values are different.
If Cells(r.Row, "B") <> Cells(1, r.Column) Then
strJob = Cells(r.Row, "B")
Set rngJob = loData.ListColumns("Job Name").DataBodyRange.Find(What:=strJob, LookAt:=xlWhole)
If Not rngJob Is Nothing Then
lngFoundRow = loData.ListRows(rngJob.Row - loData.HeaderRowRange.Row).Index
'Set the exit condition for the Do loop.
strFirstAddress = rngJob.Address
'Reset the count of users.
intTotal = 0
Do
intOccurrences = 0
strUser = loData.ListColumns("User").DataBodyRange(lngFoundRow)
'Find out if there are any occurrences where the given user has the Job Name that is presented in Row 1 on the active sheet.
intOccurrences = WorksheetFunction.CountIfs(loData.ListColumns("User").DataBodyRange, strUser, loData.ListColumns("Job Name").DataBodyRange, Cells(1, r.Column))
'If there are any undesired combinations of duties, highlighting the cell in red will cause the SOD Notifications tab to _
display the user and their conflict of duties.
If r.Interior.Color = vbRed And intOccurrences > 0 Then
With wsNotifications
.Cells(Rows.Count, "A").End(xlUp).Offset(1) = strUser
.Cells(Rows.Count, "B").End(xlUp).Offset(1) = strJob
.Cells(Rows.Count, "C").End(xlUp).Offset(1) = Cells(1, r.Column)
End With
End If
'Track the total number of users with the given Job Name combination.
intTotal = intTotal + intOccurrences
Set rngJob = loData.ListColumns("Job Name").DataBodyRange.FindNext(rngJob)
Loop Until rngJob.Address = strFirstAddress
'Print the final total in the cell.
r = intTotal
End If
End If
Next r
With Application
.ScreenUpdating = True
.Calculation = xlCalculationAutomatic
End With
End Sub
Version 2 (with arrays)
VBA Code:
Sub Check_Duties_Array()
Dim wsData As Worksheet, wsNotifications As Worksheet, r As Range, i As Long, j As Integer, intTotal As Integer
Dim msgContinue As VbMsgBoxResult, rngDuties As Range, arrData As Variant
Dim strJob As String, strCompare As String, blnNotify As Boolean
msgContinue = MsgBox(Prompt:="CAUTION!! This process can take several minutes to complete. " & _
"During this time, you will not be able to use Excel. " & _
"Are you sure you wish to continue?", _
Buttons:=vbYesNo + vbExclamation + vbDefaultButton2, _
Title:="Extended Processing Time Required")
If msgContinue = vbNo Then Exit Sub
With Application
.ScreenUpdating = False
.Calculation = xlCalculationManual
End With
'Set the major variables that will be used throughout the code
Set wsData = Sheets("Access Data")
Set wsNotifications = Sheets("SOD Notifications")
Set rngDuties = Range(Cells(2, "C"), Cells(Cells(Rows.Count, "B").End(xlUp).Row, Cells(1, Columns.Count).End(xlToLeft).Column))
'Clear any existing data to start fresh. Note that cell fill colors are intentionally left intact.
wsNotifications.Range("A2:C" & wsNotifications.Cells(Rows.Count, "A").End(xlUp).Offset(1).Row).ClearContents
rngDuties.ClearContents
'Create a 2 dimensional array with the "User" and "Job Name" fields in it.
arrData = wsData.ListObjects(1).DataBodyRange.Value2
arrData = Application.Index(arrData, Evaluate("row(1:" & UBound(arrData) & ")"), Array(1, 5))
For Each r In rngDuties
strJob = Cells(r.Row, "B")
strCompare = Cells(1, r.Column)
If r.Interior.Color = vbRed Then blnNotify = True
'Make sure that the row and column values are different.
If strJob <> strCompare Then
'Reset the count of users.
intTotal = 0
For i = 1 To UBound(arrData, 1)
If arrData(i, 2) = strJob Then
For j = 1 To UBound(arrData, 1)
If arrData(j, 1) = arrData(i, 1) And arrData(j, 2) = strCompare Then
intTotal = intTotal + 1
'If there are any undesired combinations of duties, highlighting the cell in red will cause the SOD Notifications tab to _
display the user and their conflict of duties.
If blnNotify Then
With wsNotifications
.Cells(Rows.Count, "A").End(xlUp).Offset(1) = arrData(i, 1)
.Cells(Rows.Count, "B").End(xlUp).Offset(1) = arrData(i, 2)
.Cells(Rows.Count, "C").End(xlUp).Offset(1) = strCompare
End With
End If
End If
Next j
End If
Next i
r = intTotal
blnNotify = False
End If
Next r
With Application
.ScreenUpdating = True
.Calculation = xlCalculationAutomatic
End With
End Sub