Excel VBA 逆引き集 | 差分レポート自動生成

Excel VBA
スポンサーリンク

差分レポート自動生成

「前月Aと今月Bの差分を“新規・削除・変更”に分けて、見やすいレポートを自動で作る」ためのテンプレです。初心者でも壊れないコツは「キーの正規化」「見出し名で列特定」「配列+辞書で抽出」「フォーマット整形」「必要ならPDF出力」。


ゴールと基本設計

  • 出力ゴール:
    • 新規(Bのみ), 削除(Aのみ), 変更(項目別差分)をそれぞれシートで一覧化
    • サマリー(件数・金額の合計など)を1枚に集約
    • 見やすい整形(太字・オートフィット・色分け)
    • 必要ならPDFで保存
  • 設計の柱:
    • キーは Trim+UCase で正規化
    • 見出し名から列位置を取る(列順変更に強い)
    • 配列→辞書→一括貼付で高速化
    • 数値は Val→CDbl、日付は yyyy-mm に統一

下準備ユーティリティ(速度・安全・見出し)

Option Explicit

Private Sub SpeedOn()
    Application.ScreenUpdating = False
    Application.EnableEvents = False
    Application.Calculation = xlCalculationManual
End Sub

Private Sub SpeedOff()
    Application.Calculation = xlCalculationAutomatic
    Application.EnableEvents = True
    Application.ScreenUpdating = True
End Sub

Private Function EnsureSheet(ByVal name As String, Optional ByVal clear As Boolean = True) As Worksheet
    Dim ws As Worksheet
    On Error Resume Next
    Set ws = Worksheets(name)
    On Error GoTo 0
    If ws Is Nothing Then
        Set ws = Worksheets.Add(After:=Worksheets(Worksheets.Count))
        ws.Name = name
    End If
    If clear Then ws.Cells.Clear
    Set EnsureSheet = ws
End Function

Private Function FindHeader(ByVal headerRow As Range, ByVal name As String) As Long
    Dim hit As Range
    Set hit = headerRow.Find(What:=name, LookAt:=xlWhole, LookIn:=xlValues, MatchCase:=False)
    FindHeader = IIf(hit Is Nothing, 0, hit.Column)
End Function

Private Function NormKey(ByVal v As Variant) As String
    NormKey = UCase$(Trim$(CStr(v)))
End Function
VB
  • ポイント
    • SpeedOn/Off: 無駄な再描画・再計算を止めて高速安定。
    • EnsureSheet: 何度でも安全に出力シートを用意。
    • FindHeader: 見出し名で列を特定し、列順変動に強く。
    • NormKey: Trim+UCase で表記揺れを吸収。

差分レポートの自動生成(単一キー・名称/単価の例)

AとBから新規・削除・変更を作り、サマリーも自動生成します。

Sub GenerateDiffReport()
    SpeedOn

    '入力:A (前月), B (今月)
    Dim rgA As Range: Set rgA = Worksheets("A").Range("A1").CurrentRegion
    Dim rgB As Range: Set rgB = Worksheets("B").Range("A1").CurrentRegion
    Dim vA As Variant: vA = rgA.Value
    Dim vB As Variant: vB = rgB.Value

    '見出し(キー+比較項目)
    Dim cKeyA As Long: cKeyA = FindHeader(rgA.Rows(1), "コード")
    Dim cKeyB As Long: cKeyB = FindHeader(rgB.Rows(1), "コード")
    Dim cNameA As Long: cNameA = FindHeader(rgA.Rows(1), "名称")
    Dim cNameB As Long: cNameB = FindHeader(rgB.Rows(1), "名称")
    Dim cPriceA As Long: cPriceA = FindHeader(rgA.Rows(1), "単価")
    Dim cPriceB As Long: cPriceB = FindHeader(rgB.Rows(1), "単価")
    If cKeyA*cKeyB*cNameA*cNameB*cPriceA*cPriceB = 0 Then SpeedOff: MsgBox "見出し不足(コード/名称/単価)": Exit Sub

    'B辞書(キー→(名称,単価))
    Dim dB As Object: Set dB = CreateObject("Scripting.Dictionary")
    Dim i As Long, k As String
    For i = 2 To UBound(vB, 1)
        k = NormKey(vB(i, cKeyB))
        If Len(k) > 0 Then dB(k) = Array(CStr(vB(i, cNameB)), CDbl(Val(vB(i, cPriceB))))
    Next

    'Aキー集合
    Dim setA As Object: Set setA = CreateObject("Scripting.Dictionary")
    For i = 2 To UBound(vA, 1)
        k = NormKey(vA(i, cKeyA))
        If Len(k) > 0 Then setA(k) = True
    Next

    '出力シート
    Dim wsNew As Worksheet: Set wsNew = EnsureSheet("新規(Bのみ)", True)
    Dim wsDel As Worksheet: Set wsDel = EnsureSheet("削除(Aのみ)", True)
    Dim wsChg As Worksheet: Set wsChg = EnsureSheet("変更(項目差分)", True)
    Dim wsSum As Worksheet: Set wsSum = EnsureSheet("差分サマリー", True)

    wsNew.Range("A1:D1").Value = Array("コード", "名称(B)", "単価(B)", "備考")
    wsDel.Range("A1:D1").Value = Array("コード", "名称(A)", "単価(A)", "備考")
    wsChg.Range("A1:E1").Value = Array("コード", "項目", "A値", "B値", "差分")

    Dim rNew As Long: rNew = 2
    Dim rDel As Long: rDel = 2
    Dim rChg As Long: rChg = 2

    'A基準で削除・変更を確定
    For i = 2 To UBound(vA, 1)
        k = NormKey(vA(i, cKeyA))
        If Len(k) = 0 Then GoTo nextA

        Dim aName As String: aName = CStr(vA(i, cNameA))
        Dim aPrice As Double: aPrice = CDbl(Val(vA(i, cPriceA)))

        If dB.Exists(k) Then
            Dim bName As String: bName = dB(k)(0)
            Dim bPrice As Double: bPrice = dB(k)(1)

            If aName <> bName Then
                wsChg.Cells(rChg, 1).Value = vA(i, cKeyA)
                wsChg.Cells(rChg, 2).Value = "名称"
                wsChg.Cells(rChg, 3).Value = aName
                wsChg.Cells(rChg, 4).Value = bName
                wsChg.Cells(rChg, 5).Value = "変更"
                rChg = rChg + 1
            End If
            If aPrice <> bPrice Then
                wsChg.Cells(rChg, 1).Value = vA(i, cKeyA)
                wsChg.Cells(rChg, 2).Value = "単価"
                wsChg.Cells(rChg, 3).Value = aPrice
                wsChg.Cells(rChg, 4).Value = bPrice
                wsChg.Cells(rChg, 5).Value = bPrice - aPrice
                rChg = rChg + 1
            End If
        Else
            wsDel.Cells(rDel, 1).Value = vA(i, cKeyA)
            wsDel.Cells(rDel, 2).Value = aName
            wsDel.Cells(rDel, 3).Value = aPrice
            wsDel.Cells(rDel, 4).Value = "Aのみ"
            rDel = rDel + 1
        End If
nextA:
    Next

    'Bのみ(新規)
    Dim j As Long
    For j = 2 To UBound(vB, 1)
        k = NormKey(vB(j, cKeyB))
        If Len(k) > 0 And Not setA.Exists(k) Then
            wsNew.Cells(rNew, 1).Value = vB(j, cKeyB)
            wsNew.Cells(rNew, 2).Value = vB(j, cNameB)
            wsNew.Cells(rNew, 3).Value = vB(j, cPriceB)
            wsNew.Cells(rNew, 4).Value = "Bのみ"
            rNew = rNew + 1
        End If
    Next

    'サマリー(件数・金額合計の例)
    wsSum.Range("A1:B1").Value = Array("項目", "値")
    wsSum.Range("A2:B6").Value = Array( _
        Array("新規件数", rNew - 2), _
        Array("削除件数", rDel - 2), _
        Array("変更行数", rChg - 2), _
        Array("新規単価合計", WorksheetFunction.Sum(wsNew.Range("C2:C" & rNew - 1))), _
        Array("削除単価合計", WorksheetFunction.Sum(wsDel.Range("C2:C" & rDel - 1))) _
    )

    '見やすさ整形
    Dim arrWs As Variant: arrWs = Array(wsNew, wsDel, wsChg, wsSum)
    Dim x As Variant
    For Each x In arrWs
        x.Rows(1).Font.Bold = True
        x.Columns.AutoFit
    Next

    SpeedOff
End Sub
VB
  • ポイント
    • 3分類+サマリー一括生成: 件数や金額の合計も同時に作ると配布が楽。
    • 差分列(変更の差額): 単価など数値は差分も出して可視化。

比較項目を増減できる柔軟テンプレ(見出し指定)

項目見出しの配列を渡せば、差分の対象を増減できます。数値項目は数値比較、文字項目は文字比較に自動切り替え。

Sub GenerateDiffReport_Flexible()
    SpeedOn

    Dim rgA As Range: Set rgA = Worksheets("A").Range("A1").CurrentRegion
    Dim rgB As Range: Set rgB = Worksheets("B").Range("A1").CurrentRegion
    Dim vA As Variant: vA = rgA.Value
    Dim vB As Variant: vB = rgB.Value

    Dim cKeyA As Long: cKeyA = FindHeader(rgA.Rows(1), "コード")
    Dim cKeyB As Long: cKeyB = FindHeader(rgB.Rows(1), "コード")
    If cKeyA = 0 Or cKeyB = 0 Then SpeedOff: MsgBox "キー見出し不足": Exit Sub

    '比較項目の見出し名(増減OK)
    Dim fields As Variant: fields = Array("名称", "カテゴリ", "単価", "ステータス")

    '列マップ
    Dim i As Long
    Dim mapA() As Long: ReDim mapA(LBound(fields) To UBound(fields))
    Dim mapB() As Long: ReDim mapB(LBound(fields) To UBound(fields))
    For i = LBound(fields) To UBound(fields)
        mapA(i) = FindHeader(rgA.Rows(1), fields(i))
        mapB(i) = FindHeader(rgB.Rows(1), fields(i))
        If mapA(i) = 0 Or mapB(i) = 0 Then SpeedOff: MsgBox "見出し不足:" & fields(i): Exit Sub
    Next

    'B辞書(キー→行番号)
    Dim mapBRow As Object: Set mapBRow = CreateObject("Scripting.Dictionary")
    Dim r As Long, k As String
    For r = 2 To UBound(vB, 1)
        k = NormKey(vB(r, cKeyB))
        If Len(k) > 0 Then mapBRow(k) = r
    Next

    '出力
    Dim wsChg As Worksheet: Set wsChg = EnsureSheet("変更(柔軟項目)", True)
    wsChg.Range("A1:E1").Value = Array("コード", "項目", "A値", "B値", "差分(数値のみ)")

    Dim rOut As Long: rOut = 2
    For r = 2 To UBound(vA, 1)
        k = NormKey(vA(r, cKeyA))
        If Len(k) = 0 Or Not mapBRow.Exists(k) Then GoTo contA
        Dim rb As Long: rb = mapBRow(k)

        For i = LBound(fields) To UBound(fields)
            Dim va As Variant: va = vA(r, mapA(i))
            Dim vb As Variant: vb = vB(rb, mapB(i))
            Dim isNum As Boolean: isNum = (fields(i) Like "*単価*" Or fields(i) Like "*金額*")
            Dim diff As Boolean
            If isNum Then
                diff = (CDbl(Val(va)) <> CDbl(Val(vb)))
            Else
                diff = (CStr(va) <> CStr(vb))
            End If
            If diff Then
                wsChg.Cells(rOut, 1).Value = vA(r, cKeyA)
                wsChg.Cells(rOut, 2).Value = fields(i)
                wsChg.Cells(rOut, 3).Value = va
                wsChg.Cells(rOut, 4).Value = vb
                wsChg.Cells(rOut, 5).Value = IIf(isNum, CDbl(Val(vb)) - CDbl(Val(va)), "")
                rOut = rOut + 1
            End If
        Next
contA:
    Next

    wsChg.Rows(1).Font.Bold = True
    wsChg.Columns.AutoFit
    SpeedOff
End Sub
VB
  • ポイント
    • 柔軟性: 比較対象の見出しを変えても壊れない。
    • 数値差分のみ数値計算: 誤判定や書式依存を避ける。

複合キー(コード×年月)差分レポート

年月でバージョンが変わる場合に。複合キーを作って3分類+サマリーを生成。

Private Function BuildKey2(ByVal code As Variant, ByVal ymd As Variant) As String
    Dim ym As String
    If IsDate(ymd) Then ym = Format$(CDate(ymd), "yyyy-mm") Else ym = CStr(ymd)
    BuildKey2 = NormKey(code) & "|" & UCase$(Trim$(ym))
End Function

Sub GenerateDiffReport_MultiKey()
    SpeedOn

    'A/B列想定:A=コード, B=年月, C=名称, D=単価
    Dim vA As Variant: vA = Worksheets("A").Range("A1").CurrentRegion.Value
    Dim vB As Variant: vB = Worksheets("B").Range("A1").CurrentRegion.Value

    'B辞書(複合キー→(名称,単価))
    Dim dB As Object: Set dB = CreateObject("Scripting.Dictionary")
    Dim i As Long, key As String
    For i = 2 To UBound(vB, 1)
        key = BuildKey2(vB(i, 1), vB(i, 2))
        dB(key) = Array(CStr(vB(i, 3)), CDbl(Val(vB(i, 4))))
    Next

    'Aキー集合
    Dim setA As Object: Set setA = CreateObject("Scripting.Dictionary")
    For i = 2 To UBound(vA, 1)
        setA(BuildKey2(vA(i, 1), vA(i, 2))) = True
    Next

    '出力
    Dim wsNew As Worksheet: Set wsNew = EnsureSheet("新規_複合", True)
    Dim wsDel As Worksheet: Set wsDel = EnsureSheet("削除_複合", True)
    Dim wsChg As Worksheet: Set wsChg = EnsureSheet("変更_複合", True)
    Dim wsSum As Worksheet: Set wsSum = EnsureSheet("差分サマリー_複合", True)

    wsNew.Range("A1:E1").Value = Array("コード", "年月", "名称(B)", "単価(B)", "備考")
    wsDel.Range("A1:E1").Value = Array("コード", "年月", "名称(A)", "単価(A)", "備考")
    wsChg.Range("A1:F1").Value = Array("コード|年月", "項目", "A値", "B値", "差分", "備考")

    Dim rNew As Long: rNew = 2
    Dim rDel As Long: rDel = 2
    Dim rChg As Long: rChg = 2

    For i = 2 To UBound(vA, 1)
        key = BuildKey2(vA(i, 1), vA(i, 2))
        Dim aName As String: aName = CStr(vA(i, 3))
        Dim aPrice As Double: aPrice = CDbl(Val(vA(i, 4)))

        If dB.Exists(key) Then
            Dim bName As String: bName = dB(key)(0)
            Dim bPrice As Double: bPrice = dB(key)(1)
            If aName <> bName Then
                wsChg.Cells(rChg, 1).Value = key
                wsChg.Cells(rChg, 2).Value = "名称"
                wsChg.Cells(rChg, 3).Value = aName
                wsChg.Cells(rChg, 4).Value = bName
                wsChg.Cells(rChg, 5).Value = ""
                wsChg.Cells(rChg, 6).Value = "変更"
                rChg = rChg + 1
            End If
            If aPrice <> bPrice Then
                wsChg.Cells(rChg, 1).Value = key
                wsChg.Cells(rChg, 2).Value = "単価"
                wsChg.Cells(rChg, 3).Value = aPrice
                wsChg.Cells(rChg, 4).Value = bPrice
                wsChg.Cells(rChg, 5).Value = bPrice - aPrice
                wsChg.Cells(rChg, 6).Value = "変更"
                rChg = rChg + 1
            End If
        Else
            wsDel.Cells(rDel, 1).Value = vA(i, 1)
            wsDel.Cells(rDel, 2).Value = IIf(IsDate(vA(i, 2)), Format$(CDate(vA(i, 2)), "yyyy-mm"), vA(i, 2))
            wsDel.Cells(rDel, 3).Value = aName
            wsDel.Cells(rDel, 4).Value = aPrice
            wsDel.Cells(rDel, 5).Value = "Aのみ"
            rDel = rDel + 1
        End If
    Next

    Dim j As Long
    For j = 2 To UBound(vB, 1)
        key = BuildKey2(vB(j, 1), vB(j, 2))
        If Not setA.Exists(key) Then
            wsNew.Cells(rNew, 1).Value = vB(j, 1)
            wsNew.Cells(rNew, 2).Value = IIf(IsDate(vB(j, 2)), Format$(CDate(vB(j, 2)), "yyyy-mm"), vB(j, 2))
            wsNew.Cells(rNew, 3).Value = vB(j, 3)
            wsNew.Cells(rNew, 4).Value = vB(j, 4)
            wsNew.Cells(rNew, 5).Value = "Bのみ"
            rNew = rNew + 1
        End If
    Next

    wsSum.Range("A1:B1").Value = Array("項目", "値")
    wsSum.Range("A2:B6").Value = Array( _
        Array("新規件数", rNew - 2), _
        Array("削除件数", rDel - 2), _
        Array("変更行数", rChg - 2), _
        Array("新規単価合計", WorksheetFunction.Sum(wsNew.Range("D2:D" & rNew - 1))), _
        Array("削除単価合計", WorksheetFunction.Sum(wsDel.Range("D2:D" & rDel - 1))) _
    )

    Dim arrWs As Variant: arrWs = Array(wsNew, wsDel, wsChg, wsSum)
    Dim x As Variant
    For Each x In arrWs
        x.Rows(1).Font.Bold = True
        x.Columns.AutoFit
    Next

    SpeedOff
End Sub
VB
  • ポイント
    • 年月統一: yyyy-mm 固定で揺れを防ぐ。
    • 複合キー文字連結: 区切りは「|」が安全。

レポート配布の仕上げ(PDF保存・色分け・並び替え)

必要に応じて、レポートをPDFで保存します。

Sub SaveDiffSummaryAsPDF()
    Dim ws As Worksheet: Set ws = Worksheets("差分サマリー")
    Dim path As String: path = ThisWorkbook.Path & "\差分サマリー.pdf"
    ws.ExportAsFixedFormat Type:=xlTypePDF, Filename:=path
    MsgBox "PDF保存完了: " & path
End Sub
VB
  • 見やすさの小ワザ
    • 色分け: 新規=淡緑、削除=淡赤、変更=黄色で塗ると直感的。
    • 並び替え: コード昇順や変更項目優先で並べ替えると確認が早い。

よくある落とし穴と対策

  • キー表記揺れで誤分類(新規/削除に化ける)
    • 対策: NormKey(Trim+UCase)を両側に徹底。必要なら半角化・記号除去も。
  • 数値・日付の型ズレで誤判定
    • 対策: 数値は Val→CDbl、日付は IsDate→Format(“yyyy-mm”) で統一。
  • 列順変更で壊れる
    • 対策: FindHeaderで見出しから列位置を特定。足りない場合は早期停止。
  • 大量行で遅い
    • 対策: すべて配列・辞書・一括貼付。処理前後で SpeedOn/Off。

例題で練習

'例1:単一キーで差分レポート+サマリー
Sub Example_DiffBasicReport()
    GenerateDiffReport
End Sub

'例2:見出し名で柔軟に項目を指定して差分レポート
Sub Example_DiffFlexibleReport()
    GenerateDiffReport_Flexible
End Sub

'例3:複合キー(コード×年月)で差分レポート+サマリー
Sub Example_DiffMultiKeyReport()
    GenerateDiffReport_MultiKey
End Sub

'例4:差分サマリーをPDFで保存
Sub Example_SaveDiffPDF()
    SaveDiffSummaryAsPDF
End Sub
VB

タイトルとURLをコピーしました