HiveBrain v1.2.0
Get Started
← Back to all entries
patternModerate

Comparing tables and printing discrepancies

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
tablescomparingdiscrepanciesprintingand

Problem

Here's my first VBA project. I have some very limited Python experience, and this is my first VBA project. I'm sure I could have done it a lot more simply, but I just stuck with what I knew, and googled what I didn't, so feedback is very welcome.

The basic purpose is to compare a table from one sheet (OP) with a table from another (Payroll) and print the discrepancies in a third sheet (Results).

One last thing I want to do, but can't figure out (without using GoTo, which I read is bad) is to protect the worksheets after I Exit Sub on lines 22, 31, etc. (code for protecting sheets is already written in line 130 - 138.

```
Sub Macro1()

Dim counter As Integer
Set OPdata = New Dictionary
Set Payrolldata = New Dictionary
Set HRIDnames = New Dictionary

'Unprotects sheets
Worksheets("Results").Unprotect Password:=""
Worksheets("OP").Unprotect Password:=""
Worksheets("Payroll").Unprotect Password:=""

' Clears Results worksheet
Worksheets("Results").Activate
Range("A3:L10000").ClearContents

' Looks for missing data in the OP worksheet
Worksheets("OP").Activate
Range("A2").Select
If IsEmpty(ActiveCell) Then
MsgBox "There is data missing"
Exit Sub
End If

Worksheets("OP").Activate
Range("A1").CurrentRegion.Select
For Each C In Selection
If C = "" Then
C.Select
MsgBox "There is data missing"
Exit Sub
End If
Next

' Looks for missing data in the Payroll worksheet
Worksheets("Payroll").Activate
Range("A2").Select
If IsEmpty(ActiveCell) Then
MsgBox "There is data missing"
Exit Sub
End If

Worksheets("Payroll").Activate
Range("A1").CurrentRegion.Select
For Each C In Selection
If C = "" Then
C.Select
MsgBox "There is data missing"
Exit Sub
End If
Next

' Populate OPdata dictionary
Worksheets("OP").Activate
Range("A2", ActiveCell.End(xlDown)).Select
For Each cl In Selection
OPdata.Add cl.Value, cl.Offset(0, 2).Value & " " & cl.Offset(0, 3).Value
HRIDnames.Add cl.Value, cl.Offset(0, 1).Value
Next

' Populate Payrol

Solution


  • Always put Option Explicit at the top of your modules (Tools - Options - Editor - Require Variable Declaration).



  • Never select or activate something unless it's necessary



  • Limit reading and writing to worksheets as it's an expensive operations



  • When you have code that looks similar to other code, move it to another procedure



Here's some revised code

```
Sub Macro2()

Dim dcOp As Scripting.Dictionary, dcPay As Scripting.Dictionary, dcHri As Scripting.Dictionary
Dim shPay As Worksheet, shOp As Worksheet, shResults As Worksheet
Dim vaWrite As Variant
Dim vKey As Variant
Dim lCnt As Long
Dim bFail As Boolean

'The only acceptable use of goto imho
On Error GoTo ErrHandler

Set dcOp = New Scripting.Dictionary
Set dcPay = New Scripting.Dictionary
Set dcHri = New Scripting.Dictionary

'Set sheet variables so that if a sheet name changes, you only
'have to change it in one place. Or better yet, refer to sheets
'by their codename
Set shPay = Worksheets("Payroll")
Set shOp = Worksheets("OP")
Set shResults = Worksheets("Results")

shPay.Unprotect ""
shOp.Unprotect ""
shResults.Unprotect ""

' Clears Results worksheet
'Don't activate a sheet if you don't need to
shResults.Range("A3:L10000").ClearContents

' Looks for missing data in the OP worksheet
'Value is the default property of Range, but always use it for clarity
'By raising an error, you invoke the error handler where you can do
'things like protect sheets
If IsEmpty(shOp.Range("A2").Value) Or IsEmpty(shPay.Range("A2").Value) Then
Err.Raise 9999, , "There is data missing"
End If

'Repeating code should be factored out to a different function or sub
If DataMissing(shOp.Range("A1").CurrentRegion) Or DataMissing(shPay.Range("A1").CurrentRegion) Then
Err.Raise 9999, , "There is data missing"
End If

' Populate OPdata dictionary
GetData shOp, dcOp, dcHri
GetData shPay, dcPay, dcHri

vaWrite = GetUnique(dcOp, dcPay, dcHri, bFail)
shResults.Range("A3").Resize(UBound(vaWrite, 1), UBound(vaWrite, 2)).Value = vaWrite

vaWrite = GetUnique(dcPay, dcOp, dcHri, bFail)
shResults.Range("E3").Resize(UBound(vaWrite, 1), UBound(vaWrite, 2)).Value = vaWrite

'Find salary discrepencies.
'If you're going to fill cells one-by-one, don't activate them, just offset
For Each vKey In dcOp.Keys
If dcPay.Exists(vKey) Then
If dcOp.Item(vKey) <> dcPay.Item(vKey) Then
bFail = True
With shResults.Range("I3")
.Offset(lCnt, 0).Value = vKey
If dcHri.Exists(vKey) Then
.Offset(lCnt, 1).Value = dcHri.Item(vKey)
End If
.Offset(lCnt, 2).Value = dcPay.Item(vKey)
.Offset(lCnt, 3).Value = dcOp.Item(vKey)
End With
lCnt = lCnt + 1
End If
End If
Next vKey

If Not bFail Then MsgBox "Congratulations! OurPeople and Payroll reconcile exactly!"

ErrExit:
'If no errors, this executes. If there are errors, ErrHandler resumes execution here
'so the sheets get protected no matter what
shOp.Protect
shPay.Protect
shResults.Protect
Exit Sub 'single point of exit from the sub

ErrHandler:
'Err.Raise comes here
MsgBox Err.Description
Resume ErrExit

End Sub

Private Function DataMissing(rRng As Range) As Boolean

Dim rBlanks As Range

On Error Resume Next
Set rBlanks = rRng.SpecialCells(xlCellTypeBlanks)
On Error GoTo 0

DataMissing = Not rBlanks Is Nothing

End Function

Private Sub GetData(sh As Worksheet, ByRef dcData As Scripting.Dictionary, ByRef dcExcept As Scripting.Dictionary)
'ByRef means that whatever changed you make to the variable will still be there
'when you get back to the calling procedure

Dim vaData As Variant
Dim i As Long 'VB converts all integers to Long anyway, so just use Long

'Read data once in a big chunk rather than cell-by-cell
vaData = sh.Range("A2", sh.Range("A2").End(xlDown)).Resize(, 3).Value

For i = LBound(vaData, 1) To UBound(vaData, 1)
dcData.Add vaData(i, 1), vaData(i, 3) & Space(1) & vaData(i, 4)
If Not dcExcept.Exists(vaData(i, 1)) Then
dcExcept.Add vaData(i, 1), vaData(i, 2)
End If
Next i

End Sub

Private Function GetUnique(dcFirst As Scripting.Dictionary, dcLast As Scripting.Dictionary, dcNames As Scripting.Dictionary, ByRef bFail As Boolean) As Variant

Dim aReturn() As Variant
Dim lCnt As Long
Dim vKey As Variant

ReDim aReturn(1 To dcFirst.Count, 1 To 3)

For Each vKey In dcFirst.Keys
If Not dcLast.Exists(vKey) Then
bFail = True
lCnt = lCnt + 1
aReturn(lCnt, 1) = vKey
If dcNames.Exists(vKey) Then aReturn(lCnt, 2) = dcNames.Item(vKey)
aReturn(lC

Code Snippets

Sub Macro2()

    Dim dcOp As Scripting.Dictionary, dcPay As Scripting.Dictionary, dcHri As Scripting.Dictionary
    Dim shPay As Worksheet, shOp As Worksheet, shResults As Worksheet
    Dim vaWrite As Variant
    Dim vKey As Variant
    Dim lCnt As Long
    Dim bFail As Boolean

    'The only acceptable use of goto imho
    On Error GoTo ErrHandler

    Set dcOp = New Scripting.Dictionary
    Set dcPay = New Scripting.Dictionary
    Set dcHri = New Scripting.Dictionary

    'Set sheet variables so that if a sheet name changes, you only
    'have to change it in one place. Or better yet, refer to sheets
    'by their codename
    Set shPay = Worksheets("Payroll")
    Set shOp = Worksheets("OP")
    Set shResults = Worksheets("Results")

    shPay.Unprotect ""
    shOp.Unprotect ""
    shResults.Unprotect ""

    ' Clears Results worksheet
    'Don't activate a sheet if you don't need to
    shResults.Range("A3:L10000").ClearContents

    ' Looks for missing data in the OP worksheet
    'Value is the default property of Range, but always use it for clarity
    'By raising an error, you invoke the error handler where you can do
    'things like protect sheets
    If IsEmpty(shOp.Range("A2").Value) Or IsEmpty(shPay.Range("A2").Value) Then
        Err.Raise 9999, , "There is data missing"
    End If

    'Repeating code should be factored out to a different function or sub
    If DataMissing(shOp.Range("A1").CurrentRegion) Or DataMissing(shPay.Range("A1").CurrentRegion) Then
        Err.Raise 9999, , "There is data missing"
    End If

    ' Populate OPdata dictionary
    GetData shOp, dcOp, dcHri
    GetData shPay, dcPay, dcHri

    vaWrite = GetUnique(dcOp, dcPay, dcHri, bFail)
    shResults.Range("A3").Resize(UBound(vaWrite, 1), UBound(vaWrite, 2)).Value = vaWrite

    vaWrite = GetUnique(dcPay, dcOp, dcHri, bFail)
    shResults.Range("E3").Resize(UBound(vaWrite, 1), UBound(vaWrite, 2)).Value = vaWrite

    'Find salary discrepencies.
    'If you're going to fill cells one-by-one, don't activate them, just offset
    For Each vKey In dcOp.Keys
        If dcPay.Exists(vKey) Then
            If dcOp.Item(vKey) <> dcPay.Item(vKey) Then
                bFail = True
                With shResults.Range("I3")
                    .Offset(lCnt, 0).Value = vKey
                    If dcHri.Exists(vKey) Then
                        .Offset(lCnt, 1).Value = dcHri.Item(vKey)
                    End If
                    .Offset(lCnt, 2).Value = dcPay.Item(vKey)
                    .Offset(lCnt, 3).Value = dcOp.Item(vKey)
                End With
                lCnt = lCnt + 1
            End If
        End If
    Next vKey

    If Not bFail Then MsgBox "Congratulations! OurPeople and Payroll reconcile exactly!"

ErrExit:
    'If no errors, this executes. If there are errors, ErrHandler resumes execution here
    'so the sheets get protected no matter what
    shOp.Protect
    shPay.Protect
    shResults.Protect
    Exit Sub 'single point of exit from the su

Context

StackExchange Code Review Q#32564, answer score: 10

Revisions (0)

No revisions yet.