以下の内容はhttps://www.limecode.jp/entry/fungo/074-unpivot-multiple-matrix-tablesより取得しました。


74本目:1シート複数表をDB形式に変換


Excel&VBA解説サイト「エクセルの神髄」様出題の問題集、
VBA100本ノック」に対する私の回答と解説のページです。

100本ノックの出題リストはこちらから
excel-ubara.com

出題:1シート複数表をDB形式に変換

#VBA100本ノック 74本目
「売上」シートには、A列B列に取引先コードと名称があり、その下に見出し行から始まるデータがあります。
「DB」シートにデータベース形式で出力してください。
見出し行は文字列も含め統一されています。
取引先ごとの行数は不定です。
※「DB」は既存で見出しも書式も設定済

複数のマトリクス表
変換先のデータベース

◇ 出題ページはこちら

ソースコード

定義モジュール

Option Explicit

' 売上シート
Public Enum CNo売上シート
    取引先CD = 1
    取引先名
    商品CD = 1
    商品名
    年月1st
End Enum

' DBシート
Public Const R1stDBシート = 2
Public Enum CNoDBシート
    取引先CD = 1
    取引先名
    商品CD
    商品名
    年月
    金額
End Enum

メインモジュール

Option Explicit

' 100本ノック074:1シート複数表をDB形式に変換
Sub 売上シートをテーブル形式に変換してDBシートに出力する()

    ' DBシートのクリア
    Call 指定行より下をすべて削除する(WSDB, R1stDBシート)
    
    ' 売上シートの全行をループ
    Dim R_DB As Long: R_DB = R1stDBシート
    Dim R_売上 As Long
    For R_売上 = 1 To Get最終行(WS売上)
        
        Dim R_現在の見出し As Long
        Dim 現在の取引先CD As String, 現在の取引先名 As String
        
        ' 行ごとの商品CDと商品名を取得
        Dim 現在の商品CD As String: 現在の商品CD = WS売上.Cells(R_売上, CNo売上シート.商品CD)
        Dim 現在の商品名 As String: 現在の商品名 = WS売上.Cells(R_売上, CNo売上シート.商品名)
        
        ' 見出しを検知したら見出し行番号と取引先を更新
        If 現在の商品CD = "商品CD" And 現在の商品名 = "商品名" Then
            R_現在の見出し = R_売上
            現在の取引先CD = WS売上.Cells(R_売上 - 1, CNo売上シート.取引先CD)
            現在の取引先名 = WS売上.Cells(R_売上 - 1, CNo売上シート.取引先名)
        
        ' 各データへの処理
        ElseIf 現在の商品CD <> "" And WS売上.Cells(R_売上, CNo売上シート.年月1st) <> "" Then
            
            ' 各データのすべての列をループ
            Dim C_売上 As Long
            For C_売上 = CNo売上シート.年月1st To Get最終列(WS売上)
                
                ' 見出しが日付形式の列だけを処理
                Dim 現在の年月
                現在の年月 = WS売上.Cells(R_現在の見出し, C_売上)
                If IsDate(現在の年月) Then
            
                    ' DBシートへの転記処理
                    WSDB.Cells(R_DB, CNoDBシート.取引先CD) = 現在の取引先CD
                    WSDB.Cells(R_DB, CNoDBシート.取引先名) = 現在の取引先名
                    WSDB.Cells(R_DB, CNoDBシート.商品CD) = 現在の商品CD
                    WSDB.Cells(R_DB, CNoDBシート.商品名) = 現在の商品名
                    WSDB.Cells(R_DB, CNoDBシート.年月) = 現在の年月
                    WSDB.Cells(R_DB, CNoDBシート.金額) = WS売上.Cells(R_売上, C_売上)
                    R_DB = R_DB + 1
                
                End If
                
            Next  ' データのすべての列をループ’
            
        End If
            
    Next ' 売上シートの全行をループ

End Sub

汎用関数モジュール

Option Explicit

' 指定行より下の削除
' 参考:https://www.limecode.jp/entry/utility/delete-rows-below-to-last
Sub 指定行より下をすべて削除する(対象シート As Worksheet, 指定行 As Long)
    With 対象シート
    
        Call フィルターをクリアする(対象シート)
        
        .Range(.Rows(指定行), .Rows(.Rows.Count)).Delete
        
    End With
End Sub

' フィルターのクリア
' ◆ ブログ未掲載
Sub フィルターをクリアする(対象シート As Worksheet)
    If 対象シート.AutoFilterMode = True Then
        If 対象シート.AutoFilter.FilterMode = True Then
            対象シート.AutoFilter.ShowAllData
        End If
    End If
End Sub

' 最終行の取得
' 参考:https://www.limecode.jp/entry/library/get-lastrow-lastcolumn
Function Get最終行(指定オブジェクト As Variant, Optional ByVal C As Long = -1) As Long

    ' 渡されたオブジェクトからセル範囲を取得
    Dim 対象セル範囲 As Range
    Select Case TypeName(指定オブジェクト)
    Case "Range"
        If 指定オブジェクト.Cells.CountLarge = 1 Then ' 単独セルにはCurrentRegionを取る
            Set 対象セル範囲 = 指定オブジェクト.CurrentRegion
        Else
            Set 対象セル範囲 = 指定オブジェクト
        End If
    Case "Worksheet"
        Set 対象セル範囲 = 指定オブジェクト.UsedRange
    Case "AutoFilter", "ListObject"
        Set 対象セル範囲 = 指定オブジェクト.Range
    Case Else
        Err.Raise 1000, , "対象外のオブジェクト「" & TypeName(指定オブジェクト) & "」が指定されました。"
    End Select

    ' エリアの最終行を取得
    Get最終行 = 対象セル範囲.Rows.Count + 対象セル範囲.Row - 1

    ' 列が指定されていればその列の入力最終行を取得
    If C <> -1 Then
        Do While 対象セル範囲.Worksheet.Cells(Get最終行, C) = ""
            Get最終行 = Get最終行 - 1
            If Get最終行 < 対象セル範囲.Row Then
                Get最終行 = 0
                Exit Function
            End If
        Loop
    End If

End Function

' 最終列の取得
' 参考:https://www.limecode.jp/entry/library/get-lastrow-lastcolumn
Function Get最終列(指定オブジェクト As Variant, Optional ByVal R As Long = -1) As Long

    ' 渡されたオブジェクトからセル範囲を取得
    Dim 対象セル範囲 As Range
    Select Case TypeName(指定オブジェクト)
    Case "Range"
        If 指定オブジェクト.Cells.CountLarge = 1 Then ' 単独セルにはCurrentRegionを取る
            Set 対象セル範囲 = 指定オブジェクト.CurrentRegion
        Else
            Set 対象セル範囲 = 指定オブジェクト
        End If
    Case "Worksheet"
        Set 対象セル範囲 = 指定オブジェクト.UsedRange
    Case "AutoFilter", "ListObject"
        Set 対象セル範囲 = 指定オブジェクト.Range
    Case Else
        Err.Raise 1000, , "対象外のオブジェクト「" & TypeName(指定オブジェクト) & "」が指定されました。"
    End Select

    ' エリアの最終列を取得
    Get最終列 = 対象セル範囲.Columns.Count + 対象セル範囲.Column - 1

    ' 列が指定されていればその列の入力最終行を取得
    If R <> -1 Then
        Do While 対象セル範囲.Worksheet.Cells(R, Get最終列) = ""
            Get最終列 = Get最終列 - 1
            If Get最終列 < 対象セル範囲.Column Then
                Get最終列 = 0
                Exit Function
            End If
        Loop
    End If

End Function

解説

マトリクス形式の表をデータベース形式に変換する、
いわゆる「アンピボット」と呼ばれている処理を行う問題です。

さらに今回は「マトリクス表」が複数あるため、

  • 今どのブロック(=取引先)を扱っているか
  • 今どの行(=商品)を扱っているか
  • 今どの列(=年月)を扱っているか
  • 年月を取得する先=今のブロックの見出し行の位置

を変数として記憶するループ処理を書かなければいけません。

このあたりは「現在の」などの変数名にして、
読んでわかるコードになるように心がけましょう。


入力時のインテリセンスを効かすために英字を使いたい場合は、
Currentを意味する「cur」を接頭するのもおすすめです。

Dim curR_見出し As Long
Dim cur取引先CD As String, cur取引先名 As String

Dim cur商品CD As String: cur商品CD = WS売上.Cells(R_売上, CNo売上シート.商品CD)
Dim cur商品名 As String: cur商品名 = WS売上.Cells(R_売上, CNo売上シート.商品名)

 

中身自体は愚直にデータを転記するコードになっていますので、
コードの処理に関しては特に追記はありません。


続いてデータに不備があった場合の挙動に関する懸案事項ですが、
取引先の取得に失敗した際の挙動に注意する必要があると思います。

こういったデータはどの程度同じ形になってくれているかわかりませんからね。


もし
不正なデータ
こんな風に取引先が消えていたり、
レイアウトが変わったせいで取引先行として検知できなかったとき、
前の取引先のデータとして取り込まれると困ります。


せめて「空の取引先」のデータとしてDBに出力されるよう、
見出し行を検知したところでその上の行から取引先を取得してみました。


まあこのあたりは気配りが逆に裏目になってしまうこともありますし、
どんなに気を付けても100%ユーザーの入力不備に対応することは不可能です。

色で判定したらしたで、商品にも同じ色を付けられたりとかね(´∀`;)


しっかり入力ルールを決めたり成果物をチェックする工程を設けたり、
このあたりはケースバイケースで運用でカバーしていきましょう。




以上の内容はhttps://www.limecode.jp/entry/fungo/074-unpivot-multiple-matrix-tablesより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14