比较两张纸然后输出差异 - 半成品完成

时间:2016-07-07 03:19:49

标签: excel excel-vba vba

我目前有一个将两张纸比较在一起的宏,并突出显示差异。有人可以帮我完成下一个输出到第3个文档的功能,但差异已经突出显示了吗?

列A包含Sheet1(新)和Sheet2(旧)上的唯一ID。目前Sheet1将以绿色突出显示新ID,而现有ID中的更改将在任何更改位置以黄色突出显示。

我一直在尝试添加下一个代码,其中突出显示的差异在第三张纸上生成并显示更改但没有运气。

请原谅我糟糕的编程逻辑......

Sub Compare()

Compare Macro

Const ID_COL As Integer = 1 'ID is in this column
Const NUM_COLS As Integer = 120 'how many columns are being compared?

Dim shtNew As Excel.Worksheet, shtOld As Excel.Worksheet, shtChange As Excel.Worksheet
Dim rwNew As Range, rwOld As Range, f As Range, rwRes As Range

Dim x As Integer, Id
Dim valOld, valNew

Set dict = CreateObject("Scripting.Dictionary")
Sheets.Add After:=Sheets(Sheets.Count)
ActiveSheet.Name = "Change Report"

Range("A1").Select
ActiveCell.FormulaR1C1 = "Change Type"
Selection.Font.Bold = True
Columns("A:A").EntireColumn.AutoFit

Range("B1").Select
ActiveCell.FormulaR1C1 = "ID"
Selection.Font.Bold = True
Columns("B:B").EntireColumn.AutoFit

Range("C1").Select
ActiveCell.FormulaR1C1 = "Name"
Selection.Font.Bold = True
Columns("C:C").EntireColumn.AutoFit

Range("D1").Select
ActiveCell.FormulaR1C1 = "Product"
Selection.Font.Bold = True
Columns("D:D").EntireColumn.AutoFit

Range("E1").Select
ActiveCell.FormulaR1C1 = "Old"
Selection.Font.Bold = True
Columns("E:E").EntireColumn.AutoFit

Range("F1").Select
ActiveCell.FormulaR1C1 = "New"
Selection.Font.Bold = True
Columns("F:F").EntireColumn.AutoFit

Range("G1").Select
ActiveCell.FormulaR1C1 = "Difference"
Selection.Font.Bold = True
Columns("G:G").EntireColumn.AutoFit

Sheets("Sheet1").Select
Set shtNew = ActiveWorkbook.Sheets("Sheet1")
Set shtOld = ActiveWorkbook.Sheets("Sheet2")
Set shtChange = ActiveWorkbook.Sheets("Change Report")


ActiveWorkbook.Worksheets("Sheet1").AutoFilterMode = False
ActiveWorkbook.Worksheets("Sheet2").AutoFilterMode = False
ActiveWorkbook.Worksheets("Change Report").AutoFilterMode = False

Set rwNew = shtNew.Rows(2) 'first entry on "current" sheet
Set rwRes = shtChange.Rows(2)

ActiveWorkbook.Worksheets("Sheet1").AutoFilterMode = False
ActiveWorkbook.Worksheets("Sheet2").AutoFilterMode = False

Do While rwNew.Cells(ID_COL).Value <> "" 'Compares new Sheet to old Sheet

    rwRes.EntireRow(x).Value = rwNew.EntireRow(x).Value

    Id = rwNew.Cells(ID_COL).Value

    Set f = shtOld.UsedRange.Columns(ID_COL).Find(Id, , xlValues, xlWhole)
    If Not f Is Nothing Then
        Set rwOld = f.EntireRow

        For x = 1 To NUM_COLS
        r = 1

            If rwNew.Cells(x).Value <> rwOld.Cells(x).Value Then
                rwNew.Cells(x).Interior.Color = vbYellow

                'rwRes.Cells(r, 2).Value = rwNew.Cells(x, 1).Value 'ID
                'rwRes.Cells(r, 3).Value = rwNew.Cells(x, 11).Value 'Name
                'rwRes.Cells(r, 4).Value = rwNew.Cells(x, 12).Value 'Product
                'rwRes.Cells(r, 5).Value = rwOld.Cells(x, 14).Value 'Price old
                'rwRes.Cells(r, 6).Value = rwNew.Cells(x, 14).Value 'Price new
                'Percentage Change from old to new 'Difference

                r = r + 1
            Else
                rwNew.Cells(x).Interior.ColorIndex = xlNone
            End If
        Next x

    Else
        rwNew.EntireRow.Interior.Color = vbGreen 'new entry
         'rwRes.Cells(r, x).Value = rwNew.Cells(x, 1).Value 
         'rwRes.Cells(r, 2).Value = rwNew.Cells(x, 1).Value 'ID
         'rwRes.Cells(r, 3).Value = rwNew.Cells(x, 11).Value 'Name
         'rwRes.Cells(r, 4).Value = rwNew.Cells(x, 12).Value 'Product
         'rwRes.Cells(r, 6).Value = rwNew.Cells(x, 14).Value 'Price

        r = r + 1

    End If


    Set rwNew = rwNew.Offset(1, 0) 'next row to compare

    Loop

   Selection.AutoFilter
MsgBox ("Complete")
End Sub

2 个答案:

答案 0 :(得分:0)

您需要设置对Microsoft Scripting Runtime的引用。

这应该非常接近你想要的。

  • ProductRecord:存储要写入新工作表的所有变量
  • dProducts:是一个包含ProductRecords的字典
  • 迭代Sheet1如果它们的单元格是彩色的,则按ID将产品添加到dProducts
  • 迭代Sheet2按ID搜索dProducts。如果找到,我们会设置产品的旧价格
  • 迭代工作表(&#34;更改报告&#34;)随时随地将产品粘贴到dProducts中

Class ProductRecord

Option Explicit

Public ChangeType As String
Public ID As String
Public Name As String
Public Product As String
Public OldPrice As Double
Public NewPrice As Double
Public Difference As Double
Public Color As Long

Public Sub Paste(Destination As Range)
    Dim arData(5)
    Difference = NewPrice - OldPrice
    If Color = vbGreen Then ChangeType = "New Product" Else ChangeType = "ID Change"
    arData(0) = ChangeType
    arData(1) = Name
    arData(2) = Product
    arData(3) = OldPrice
    arData(4) = NewPrice
    arData(5) = Difference

    Destination.Resize(1, 6) = arData 'WorksheetFunction.Transpose(arData)
    Destination.Interior.Color = Color

End Sub

故事的其余部分

选项明确

Sub Compare()
    ToggleEvents False
    Dim shtNew As Excel.Worksheet, shtOld As Excel.Worksheet, shtChange As Excel.Worksheet
    Dim rwNew As Range
    Dim k As String
    Dim lastRow As Long, x As Long, y
    Dim Product As ProductRecord
    Dim dProducts As Dictionary
    Set dProducts = New Dictionary

    Set shtNew = Sheets("Sheet1")
    Set shtOld = Sheets("Sheet2")

    shtNew.AutoFilterMode = False
    shtOld.AutoFilterMode = False

    With shtNew
        lastRow = .Range("A" & Rows.Count).End(xlUp).Row
        For x = 2 To lastRow
            For Each y In Array(1, 11, 12, 14)
                If .Cells(x, y).Interior.color = vbYellow Or .Cells(x, y).Interior.color = vbGreen Then
                    Set Product = New ProductRecord
                    k = .Cells(x, 1).Value
                    Product.color = .Cells(x, y).Interior.color
                    Product.ID = .Cells(x, 1).Value    'ID
                    Product.Name = .Cells(x, 11).Value    'Name
                    Product.Product = .Cells(x, 12).Value    'Product
                    Product.NewPrice = .Cells(x, 14).Value    'Price old

                    If Not dProducts.Exists(k) Then
                        dProducts.Add k, Product
                        Exit For
                    End If
                End If
            Next
        Next
    End With

    If dProducts.Count > 0 Then
        With shtOld
            lastRow = .Range("A" & Rows.Count).End(xlUp).Row
            For x = 2 To lastRow
                k = .Cells(x, 1).Value

                If dProducts.Exists(k) Then

                     dProducts(k).OldPrice = .Cells(x, 14).Value    'ID

                End If
            Next

        End With
    End If

    Set shtChange = getChangeReportWorkSheet

    With shtChange.Range("A1:G1")
        .Value = Array("Change Type", "ID", "Name", "Product", "Old", "New", "Difference")
        Selection.Font.Bold = True
    End With

    With shtChange
        lastRow = dProducts.Count - 1
        For x = 0 To lastRow

            dProducts.Items(x).Paste .Cells(x + 2, 1)

        Next
        .Range("A1:G1").EntireColumn.AutoFit
    End With

    ToggleEvents True
    'Selection.AutoFilter
    MsgBox ("Complete")
End Sub

Sub ToggleEvents(EnableEvents As Boolean)

    With Application
        .EnableEvents = EnableEvents
        .Calculation = IIf(EnableEvents, xlCalculationAutomatic, xlCalculationManual)
    End With

End Sub

Function getChangeReportWorkSheet() As Worksheet
    Application.DisplayAlerts = False
    On Error Resume Next
    Worksheets("Change Report").Delete
    On Error GoTo 0
    Application.DisplayAlerts = True
    Set getChangeReportWorkSheet = Sheets.Add(After:=Sheets(Sheets.Count))
    getChangeReportWorkSheet.Name = "Change Report"
End Function

答案 1 :(得分:0)

作为Thomas发布的解决方案的替代方案,您可以使用词典来存储每个唯一ID和相关列的索引。通过基于硬编码数组(vHeader和vLookFor)和range.find方法的循环中的字典填充,这使您可以更改列的位置以及代码的某种程度行为,而无需担心索引会进一步下降。

该脚本首先填充新旧工作表的标题和ID的字典,然后循环新的ID键,以找到对任何设置为相关字段的字段进行更改的字典。 vLookFor,以及全新的。

在创建shtChange标头范围时使用函数columnLetter可确保如果向vheader添加字段,它将自动添加到shtChange.To避免必须删除shtChange以防您想要重新运行宏,我添加了一个doExist函数 - 它只是删除工作表并返回一个同名的新工作表对象。

如果识别出差异或新字段,则将该行移至shtChange并计算差值(新价格/旧价格以%表示)。

更改列的顺序将在当前破坏您通过字段检查所有120列,但您可以更新它以使用字典,或更具体地range.find,减轻用户倾向于做的事情(移动列,排序等) - 但责怪你。

Sub Compare()
    'reference to Microsoft scripting runtime is a prerequisite for Dictionaries to work

    'can the shtOld.usedrange.columns.count potentially substitute this hardcode?
    Const ID_COL As Integer = 1 'ID is in this column
    Const NUM_COLS As Integer = 120 'how many columns are being compared

    Dim shtNew As Worksheet, shtOld As Worksheet, shtChange As Worksheet
    Dim vHeader As Variant
    Dim vLookFor As Variant
    Dim vElement As Variant
    Dim vKeyID As Variant
    Dim vKeyValueIdx As Variant
    Dim oldRowIdx As Variant
    Dim oldColIdx As Variant
    Dim newRowIdx As Variant
    Dim newColIdx As Variant
    Dim chgRowIdx As Long
    Dim oldPriceIdx As Long
    Dim newPriceIdx As Long
    Dim diffPriceIdx As Long
    Dim chgTypeIdx As Long
    Dim shtChangeName As String
    Dim oldIndexDict As Dictionary
    Dim oldIdRowDict As Dictionary
    Dim newIndexDict As Dictionary
    Dim newIdRowDict As Dictionary
    Dim chgIndexDict As Dictionary
    Dim i As Long, j As Long, k As Long, m As Long, n As Long

    Dim x As Integer, Id
    Dim valOld, valNew

    'some intital parameters
    shtChangeName = "Change Report"

    'rather than printing the header one value at a time, then you can simply place an array directly into the range
    vHeader = Array("Change Type", "ID", "Name", "Product", "Old Price", "New Price", "Difference")

    'we create a array for the headers that we will be looking for, for the shtChange
    vLookFor = Array("ID", "Name", "Product", "Price")

    'setting the worksheet object
    Set shtNew = ThisWorkbook.Sheets("Sheet1")
    Set shtOld = ThisWorkbook.Sheets("Sheet2")

    'add the shtChange
    Set shtChange = doExist(shtChangeName) 'I really hate having to manually delete a worksheets in case I want to rerun, so I added the doExist function to delete the sheet if it allready exist

    'disable any data fitler
    shtNew.AutoFilterMode = False
    shtOld.AutoFilterMode = False

    'Generating the bold headers for the change sheet, to avoid retyping the range over and over again, we use with
    With shtChange.Range("A1:" & ColumnLetter(UBound(vHeader) + 1) & "1") 'this is implicitly repeated for all rows, e.g. '.value' -> 'shtChange.Range("A1:G1").value'
        .Value = vHeader
        .Font.Bold = True
    End With

    'I will be using dictionaries to find my way around the position of specific headers and ID's. This I do for added robustness, in case the business decides to move columns, change the sorting etc. in only the old or new sheet
    Set oldIndexDict = CreateObject("Scripting.Dictionary") 'for header index
    Set oldIdRowDict = CreateObject("Scripting.Dictionary") 'for ID row index
    Set newIndexDict = CreateObject("Scripting.Dictionary") 'for header index
    Set newIdRowDict = CreateObject("Scripting.Dictionary") 'for ID row index
    Set chgIndexDict = CreateObject("Scripting.Dictionary") 'for header index

    'we populate the index dictionaries
    For Each vElement In vLookFor
        If Not newIndexDict.Exists(CStr(vElement)) Then
            oldIndexDict.Add CStr(vElement), shtOld.Range("1:1").Find(what:=CStr(vElement), LookIn:=xlValues, LookAt:=xlWhole).Column
            newIndexDict.Add CStr(vElement), shtNew.Range("1:1").Find(what:=CStr(vElement), LookIn:=xlValues, LookAt:=xlWhole).Column
            On Error Resume Next
            chgIndexDict.Add CStr(vElement), shtChange.Range("1:1").Find(what:=CStr(vElement), LookIn:=xlValues, LookAt:=xlWhole).Column
            On Error GoTo 0
        End If
    Next

    'In case the data is not ordered exactly the same in the new and old sheets, we populate the IdRow dictionaries to enable us to find the position of a specific ID in either sheet
        'first the oldSht
        For i = 2 To shtOld.UsedRange.Rows.Count 'be aware that if your data does not start on row 1, the usedrange will not accurately reflect the true last row number
            If Not oldIdRowDict.Exists(CStr(shtOld.Cells(i, oldIndexDict("ID")))) And CStr(shtOld.Cells(i, oldIndexDict("ID"))) <> "" Then
                oldIdRowDict.Add CStr(shtOld.Cells(i, oldIndexDict("ID"))), i
            End If
        Next

        'then the newSht
        For j = 2 To shtNew.UsedRange.Rows.Count 'be aware that if your data does not start on row 1, the usedrange will not accurately reflect the true last row number
            If Not newIdRowDict.Exists(CStr(shtNew.Cells(j, newIndexDict("ID")))) And CStr(shtNew.Cells(j, newIndexDict("ID"))) <> "" Then
                newIdRowDict.Add CStr(shtNew.Cells(j, newIndexDict("ID"))), j
            End If
        Next

    'get indexes for fields specific for shtChange
    chgTypeIdx = shtChange.Range("1:1").Find(what:="Change Type", LookIn:=xlValues, LookAt:=xlWhole).Column 'index for changetype
    oldPriceIdx = shtChange.Range("1:1").Find(what:="Old Price", LookIn:=xlValues, LookAt:=xlWhole).Column 'index for old price
    newPriceIdx = shtChange.Range("1:1").Find(what:="New Price", LookIn:=xlValues, LookAt:=xlWhole).Column 'indexd for new price
    diffPriceIdx = shtChange.Range("1:1").Find(what:="Difference", LookIn:=xlValues, LookAt:=xlWhole).Column 'index for difference column

    'then we loop the keys in the New sheet and make the relevant comparision, incl. move to shtChange
    For Each vKeyID In newIdRowDict.Keys
        'retrieve the relevant indexes for the columns going into the shtChange
        newRowIdx = newIdRowDict(vKeyID)

        If oldIdRowDict.Exists(vKeyID) Then
            oldRowIdx = oldIdRowDict(vKeyID)

            For Each vKeyValueIdx In newIndexDict.Keys
                If shtOld.Cells(oldRowIdx, oldIndexDict(vKeyValueIdx)) <> shtNew.Cells(newRowIdx, newIndexDict(vKeyValueIdx)) Then
                    chgRowIdx = shtChange.UsedRange.Rows.Count + 1
                    shtChange.Cells(chgRowIdx, chgTypeIdx) = "Update" 'the key allready existed in the old sheet, so update

                    For m = LBound(vLookFor) To UBound(vLookFor)
                        If chgIndexDict.Exists(vLookFor(m)) Then
                            shtChange.Cells(chgRowIdx, chgIndexDict(vLookFor(m))) = shtNew.Cells(newRowIdx, newIndexDict(vLookFor(m)))
                        End If
                    Next

                    shtChange.Cells(chgRowIdx, oldPriceIdx) = shtOld.Cells(oldRowIdx, oldIndexDict("Price"))
                    shtChange.Cells(chgRowIdx, newPriceIdx) = shtNew.Cells(newRowIdx, newIndexDict("Price"))
                    shtChange.Cells(chgRowIdx, diffPriceIdx) = shtChange.Cells(chgRowIdx, newPriceIdx) / shtChange.Cells(chgRowIdx, oldPriceIdx)
                End If
            Next

            shtChange.Columns(diffPriceIdx).NumberFormat = "0.0%"

            'This is subject to risk of moved columns etc., but to retain functionality of the posted code we loop all columns the respective ID, and set the colors
            For k = 1 To NUM_COLS
                If shtOld.Cells(oldRowIdx, k).Value <> shtNew.Cells(newRowIdx, k).Value Then
                    shtNew.Cells(newRowIdx, k).Interior.Color = vbYellow
                Else
                    shtNew.Cells(newRowIdx, k).Interior.ColorIndex = xlNone
                End If
            Next
        Else 'it is a new entry
            shtNew.Range("A" & newRowIdx).EntireRow.Interior.Color = vbGreen 'new entry
            chgRowIdx = shtChange.UsedRange.Rows.Count + 1

            For n = LBound(vLookFor) To UBound(vLookFor) 'loops the elements of the search fields, and if they exist in shtChange, we fetch the value from shtNew
                If chgIndexDict.Exists(vLookFor(n)) Then
                    shtChange.Cells(chgRowIdx, chgIndexDict(vLookFor(n))) = shtNew.Cells(newRowIdx, newIndexDict(vLookFor(n)))
                End If
            Next

            shtChange.Cells(chgRowIdx, chgTypeIdx) = "New" 'key is new, so New
            shtChange.Cells(chgRowIdx, newPriceIdx) = shtNew.Cells(newRowIdx, newIndexDict("Price")) 'since the element is new, only the new price is relevant for shtChange

        End If
    Next

    shtChange.Range("A1:G1").Columns.AutoFit
    shtChange.Range("A1").AutoFilter

    'set the dicts to nothing
    Set oldIndexDict = Nothing
    Set oldIdRowDict = Nothing
    Set newIndexDict = Nothing
    Set newIdRowDict = Nothing
    Set chgIndexDict = Nothing

    MsgBox ("Complete")
End Sub



Function doExist(strSheetName) As Worksheet
    Dim wb As Workbook: Set wb = ThisWorkbook
    Dim wsTest As Worksheet
    Dim nWs As Worksheet

    Set wsTest = Nothing
    On Error Resume Next
    Set wsTest = wb.Worksheets(strSheetName)
    On Error GoTo 0

    If Not wsTest Is Nothing Then
        Application.DisplayAlerts = False
        wsTest.Delete
        Application.DisplayAlerts = True
    End If

    Set doExist = wb.Worksheets.Add(after:=wb.Sheets(wb.Sheets.Count))
    doExist.Name = strSheetName

End Function


Function ColumnLetter(ColumnNumber As Long) As String
    Dim n As Long
    Dim c As Byte
    Dim s As String

    n = ColumnNumber
    Do
        c = ((n - 1) Mod 26)
        s = Chr(c + 65) & s
        n = (n - c) \ 26
    Loop While n > 0
    ColumnLetter = s
End Function