[VBA]Dictionary(連想配列)のソートを考える

VBA

VBAでDictionary(連想配列)を使っていると格納済みのデータをキーで並べ替えたい局面に遭遇することってありませんか?

しかしVBAのDictionary(連想配列)オブジェクトには残念ながらソート機能がサポートされていないため、さてどうしたものかと悩んでしまう方もいるのではないでしょうか。

プログラムの作成において使用するオブジェクトのメソッドで機能がサポートされていないのならば別の方法で問題を解決していくしかありません。

今回はDictionary(連想配列)でソートが使えないならこうやってソートを実現すればいいんじゃないかな?というテーマを取り上げてみたいと思います。

いくつかソートの方法をまとめてみたのでDictionary(連想配列)のソートでお悩みの方はぜひ参考にして頂ければ幸いです。

スポンサーリンク
スポンサーリンク

Dictionary(連想配列)のソートについて

Dictionary(連想配列)のソートについては上でもさらっと触れましたが、オブジェクトのメソッドとして残念ながらサポートされておりません。

Dictionary(連想配列)はとても便利なオブジェクトなのですが、ソートができないという欠点があるため、ソートを必要としない処理であれば問題ないですが、ソートが必要とされる処理の場合には何らかの策が必要となります。

単純にソートロジックをVBAで作りこむか、他のオブジェクトの仕組みにソート部分をお願いするか、2点が考えつく策になると思います。

今回のソート方法を考えるあたって前提条件として以下1点を条件とします。

Dictionary(連想配列)のキーには文字列型を設定すること

当たり前のことですが、プログラム作成をする際に仕様を決めておくことは大切なことです。

Dictionary(連想配列)のキーには、文字列型や数値型、オブジェクト型なども設定することが可能であるため、そもそもソート機能を簡単に定義できない問題があります。

そのため今回のサンプルではシンプルに単純な文字列比較でソートしていく手法を検討いくため、キーに設定できるデータ型は文字列型に限定した仕様とさせて頂きます。

サンプルで使用するリストは、以下の国名リストを使ってそれぞれのソート方法を考えてみたいと思います。

日本語英語
日本Japan
アメリカAmerica
中国China
イギリスEngland
ドイツGermany
オーストラリアAustralia
カナダCanada

ソートロジック(クイックソート)でソートする

ソートアルゴリズムは基本中の基本なのでこのサイトでも取り上げてみたいと思います。

Sub Sample_001()

    Dim objDic As New Dictionary
    Dim arrKeys As Variant
    Dim arrList() As Variant
    Dim i As Integer
 
    'Dictionary(連想配列)にデータを格納
    objDic.Add "日本", "Japan"
    objDic.Add "アメリカ", "America"
    objDic.Add "中国", "China"
    objDic.Add "イギリス", "England"
    objDic.Add "ドイツ", "Germany"
    objDic.Add "オーストラリア", "Australia"
    objDic.Add "カナダ", "Canada"
    
    'Dictionary(連想配列)キーを1次元配列化
    arrKeys = objDic.Keys
    
    'ソート用の2次元配列を定義
    ReDim arrList(objDic.Count - 1, 1)
    
    'キーとデータをソート用配列に取り込み
    For i = LBound(arrKeys) To UBound(arrKeys)
        'キーとデータを追加
        arrList(i, 0) = arrKeys(i)
        arrList(i, 1) = objDic(arrKeys(i))
        'キーとデータをデバッグ出力
        Debug.Print arrList(i, 0) & ":" & arrList(i, 1)
    Next
    
    '2次元配列をクイックソート
    Call QuickSort(arrList, LBound(arrList, 1), UBound(arrList, 1))
    
    'Dictionary(連想配列)の全ペアを削除
    objDic.RemoveAll
    
    Debug.Print "====================="
    
    'ソート用配列からキーとデータをDictionary(連想配列)に取り込み
    For i = LBound(arrList) To UBound(arrList)
        'キーとデータを追加
        objDic.Add arrList(i, 0), arrList(i, 1)
    Next

    'Dictionary(連想配列)キーを1次元配列化
    arrKeys = objDic.Keys

    'ソート結果の出力
    For i = LBound(arrKeys) To UBound(arrKeys)
        'キーとデータをデバッグ出力
        Debug.Print arrKeys(i) & ":" & objDic.Item(arrKeys(i))
    Next

    'Dictionary(連想配列)を開放
    Set objDic = Nothing
  
End Sub

Sub QuickSort(ByRef arrList() As Variant, ByVal minIDX As Long, ByVal maxIDX As Long)

    Dim valMEDIAN As Variant
    Dim arrTEMP() As Variant
    Dim i As Long
    Dim j As Long
    
    'ソート範囲の上下限を設定
    i = minIDX
    j = maxIDX
    
    '基準値としてインデックスの中央値のデータを使用します。
    valMEDIAN = arrList(Int((minIDX + maxIDX) / 2), 0)

    Do
        'インデックスの小さい方から値を比較していく
        Do While StrComp(arrList(i, 0), valMEDIAN) < 0
            i = i + 1
        Loop
        
        'インデックスの大きい方から値を比較していく
        Do While StrComp(arrList(j, 0), valMEDIAN) > 0
            j = j - 1
        Loop
        
        'インデックスが逆転したらループ抜け
        If i >= j Then Exit Do
        
        '配列を定義
        ReDim arrTEMP(0, 1)
        
        '配列内のデータの入れ替え1
        arrTEMP(0, 0) = arrList(i, 0)
        arrList(i, 0) = arrList(j, 0)
        arrList(j, 0) = arrTEMP(0, 0)
        
        '配列内のデータの入れ替え2
        arrTEMP(0, 1) = arrList(i, 1)
        arrList(i, 1) = arrList(j, 1)
        arrList(j, 1) = arrTEMP(0, 1)
        
        '配列の初期化
        Erase arrTEMP
        
        'インデックスの加減算
        i = i + 1
        j = j - 1
    Loop
    
    '再帰でソートが完了するまで繰り返し
    If (minIDX < i - 1) Then Call QuickSort(arrList, minIDX, i - 1)
    If (maxIDX > j + 1) Then Call QuickSort(arrList, j + 1, maxIDX)
    
End Sub

この処理の補足

Dictionary(連想配列)のソートはキーを基準に格納されているペアを並べ替える処理になります。

クイックソートは回帰ループ処理の代表格のようなベーシックなロジックですが、速度も速いし、理解を進めながら組み込んでいくと達成感が湧いてくるロジックです。(私の場合ですが)

結果は以下のとおりです。
Dictionary(連想配列)のクイックソートの結果(キー)
ソートの並び順(昇順、降順)は再取り込みの際のループの回し方で調整することができます。

クイックソートはソートアルゴリズムの中では早い方ですが、データ量によりやや処理速度にぶれがあるので、対象データに合わせて採用するか否か検討する必要があります。

正直言えばこれでほとんど問題のないレベルかもしれません。

.NET Framework のArrayList クラスでソートする

ソートロジックをごりごり組まずにやり過ごす場合におすすめなのが、.NET Framework のArrayList クラスを使ったソートです。

Sub Sample_002()

    Dim objDic As New Dictionary
    Dim arrKeys As Variant
    Dim arrSort As Variant
    Dim objList As Object
    Dim varItem As Variant
    Dim i As Integer
 
    'Dictionary(連想配列)にデータを格納
    objDic.Add "日本", "Japan"
    objDic.Add "アメリカ", "America"
    objDic.Add "中国", "China"
    objDic.Add "イギリス", "England"
    objDic.Add "ドイツ", "Germany"
    objDic.Add "オーストラリア", "Australia"
    objDic.Add "カナダ", "Canada"
    
    'Dictionary(連想配列)キーを1次元配列化
    arrKeys = objDic.Keys
    'SortedListオブジェクトを生成
    Set objList = CreateObject("System.Collections.ArrayList")
    
    'キーをソート用ArrayListに取り込み
    For i = LBound(arrKeys) To UBound(arrKeys)
        'ArrayListにキーを追加
        objList.Add arrKeys(i)
        'キーとデータをデバッグ出力
        Debug.Print arrKeys(i) & ":" & objDic.Item(arrKeys(i))
    Next
    
    'ArrayListをソート
    objList.Sort
    'ArrayListを配列に変換
    arrSort = objList.ToArray

    Debug.Print "====================="
    
    For i = LBound(arrSort) To UBound(arrSort)
        'データを抽出
        varItem = objDic.Item(arrSort(i))
        'もともと格納されているペアを削除
        objDic.Remove arrSort(i)
        '新規ペアとして追加
        objDic.Add arrSort(i), varItem
    Next

    'Dictionary(連想配列)キーを1次元配列化
    arrKeys = objDic.Keys
    
    'ソート結果の出力
    For i = LBound(arrKeys) To UBound(arrKeys)
        'キーとデータをデバッグ出力
        Debug.Print arrKeys(i) & ":" & objDic.Item(arrKeys(i))
    Next
    
    'ArrayListの格納データを全削除
    objList.Clear
    
    'ArrayListを開放
    Set objList = Nothing
    'Dictionary(連想配列)を開放
    Set objDic = Nothing
  
End Sub

この処理の補足

VBAでも呼び出すことが出来る動的配列のArrayListは大変便利なオブジェクトです。

このサンプルではレイトバインディングでArrayListオブジェクトを生成しています。

アーリーバインディングでのオブジェク生成も可能ですが、参照設定のリスト内にデフォルトでは含まれておらず、話が横道にそれてしまうのでこの説明からは割愛させていただきます。

通常VBAで動的配列を処理しているとReDimPreserveなどで配列のサイズを変更していく必要があるのですが、このArrayListはDictionary(連想配列)やCollectionなどのようにAddメソッドでデータを追加していくことが可能です。

さらにSortメソッドやArrayListから配列へ変換するメソッドもサポートされています。

上記サンプルのようにArrayListを活用することでソート部分を解決させることができるというわけです。

※オートメーションエラーについては、ArrayListの紹介記事で詳しく取り上げたいと思います。

[VBA]ArrayListの使い方を知れば動的配列も解決!
VBAで.Net FrameworkのArraListクラスが使えるのはご存じですか?このページではVBAでArrayListを使うための基本的な部分をご紹介するとともに動的配列の代替手段としてArrayListが耐えられるかを検証しています。

処理の結果は以下のとおりです。
SortedListでのソートの結果

OnMemoryRecordsetを使ってソートする

VBAを使っている人には馴染みのあるADOのレコードセットを使ってソートする方法です。

Sub Sample_003()

    Dim objDic As New Dictionary
    Dim objRst As New ADODB.Recordset
    Dim arrKeys As Variant
    Dim i As Integer
 
    'Dictionary(連想配列)にデータを格納
    objDic.Add "日本", "Japan"
    objDic.Add "アメリカ", "America"
    objDic.Add "中国", "China"
    objDic.Add "イギリス", "England"
    objDic.Add "ドイツ", "Germany"
    objDic.Add "オーストラリア", "Australia"
    objDic.Add "カナダ", "Canada"
    
    'Dictionary(連想配列)キーを1次元配列化
    arrKeys = objDic.Keys
      
    'レコードセットを定義する
    objRst.Fields.Append "キー", adVarChar, 255
    objRst.Fields.Append "データ", adVarChar, 255
    
    'レコードセットをオープン
    objRst.Open
     
    'レコードセットへキーとデータを取り込み
    For i = LBound(arrKeys) To UBound(arrKeys)
        'キーとデータへ追加
        objRst.AddNew
        objRst.Fields("キー").value = arrKeys(i)
        objRst.Fields("データ").value = objDic.Item(arrKeys(i))
        objRst.Update
        'キーとデータをデバッグ出力
        Debug.Print arrKeys(i) & ":" & objDic.Item(arrKeys(i))
    Next
    
    'レコードセットをソート
    objRst.Sort = "キー ASC"
    
    'Dictionary(連想配列)の全ペアを削除
    objDic.RemoveAll

    Debug.Print "====================="
    
    'Dictionary(連想配列)へソート済みペアの取り込み
    objRst.MoveFirst
    Do Until objRst.EOF
        'キーとデータを追加
        objDic.Add objRst.Fields("キー").value, objRst.Fields("データ").value
        objRst.MoveNext
    Loop
    objRst.Close

    'Dictionary(連想配列)キーを1次元配列化
    arrKeys = objDic.Keys
    
    'ソート結果の出力
    For i = LBound(arrKeys) To UBound(arrKeys)
        'キーとデータをデバッグ出力
        Debug.Print arrKeys(i) & ":" & objDic.Item(arrKeys(i))
    Next
    
    'レコードセットを開放
    Set objRst = Nothing
    'Dictionary(連想配列)を開放
    Set objDic = Nothing
  
End Sub

この処理の補足

通常だとレコードセットはSQLを通じて取得した結果を返すオブジェクトとして広く使われていますが、非接続のレコードセットを使ってソートの処理を解決させる方法もあるわけです。

正直いえばクイックソートやArrayListの方が処理は早いのではないかとは思うのですが、ソートのやり方も様々な方法があるという例で挙げさせてもらいました。

普段VBAでデータベースを操作している方などはADO操作をする方もいるでしょうし、単純にADO好きな方や一連の処理をなるべくADOで統一したい方(どんな方?)はぜひ検討の一つに加えてみても面白いかもしれません。

結果は以下のとおりです。
レコードセットを使ったソート結果

Excelのソートメソッドを使ってソートする

もしEXCEL上でVBAを使用している場合に恩恵が得られるソートのご紹介です。

Sub Sample_004()

    Dim objDic As New Dictionary
    Dim arrKeys As Variant
    Dim arrSort As Variant
    Dim varItem As Variant
    Dim i As Integer
 
    'Dictionary(連想配列)にデータを格納
    objDic.Add "日本", "Japan"
    objDic.Add "アメリカ", "America"
    objDic.Add "中国", "China"
    objDic.Add "イギリス", "England"
    objDic.Add "ドイツ", "Germany"
    objDic.Add "オーストラリア", "Australia"
    objDic.Add "カナダ", "Canada"
    
    'Dictionary(連想配列)キーを1次元配列化
    arrKeys = objDic.Keys
         
    'ソート前の出力結果
    For i = LBound(arrKeys) To UBound(arrKeys)
        'キーとデータをデバッグ出力
        Debug.Print arrKeys(i) & ":" & objDic.Item(arrKeys(i))
    Next
    
    'Excelのソートメソッドを使ったキーの並び替え
    Application.DisplayAlerts = False
    'テンポラリーシートを追加
    With ThisWorkbook.Worksheets.Add
        'シートへキー配列を取り込み
        .Range(.Cells(1, 1), .Cells(objDic.Count, 1)) = WorksheetFunction.Transpose(arrKeys)
        'シートメソッドを実施
        .Range("A1").CurrentRegion.Sort Key1:=Range("A1"), Order1:=xlAscending, Header:=xlNo
        'ソート結果を配列に取り込み
        arrSort = .Range("A1").CurrentRegion.value
        'テンポラリーシートを削除
        .Delete
    End With
    Application.DisplayAlerts = True
    
    Debug.Print "====================="
    
    For i = LBound(arrSort) To UBound(arrSort)
        'データを抽出
        varItem = objDic.Item(arrSort(i, 1))
        'もともと格納されているペアを削除
        objDic.Remove arrSort(i, 1)
        '新規ペアとして追加
        objDic.Add arrSort(i, 1), varItem
    Next
    
    'Dictionary(連想配列)キーを1次元配列化
    arrKeys = objDic.Keys
    
    'ソート結果の出力
    For i = LBound(arrKeys) To UBound(arrKeys)
        'キーとデータをデバッグ出力
        Debug.Print arrKeys(i) & ":" & objDic.Item(arrKeys(i))
    Next

    'Dictionary(連想配列)を開放
    Set objDic = Nothing
  
End Sub

この処理の補足

今回はEXCELでVBAを作成している想定でのサンプルになります。

EXCELのオブジェクトを生成すればEXCEL以外からでもソートメソッドは使用可能ですが、オブジェクト生成、ブック生成などソートメソッドにたどり着くまでの処理に時間が掛かりますので、EXCELでのVBA以外は他の方法も合わせて検討してください。

ただしEXCEL上でのVBA操作の場合だと、データ量が膨大でもExcelの標準機能を活用するためなかなか良好な結果が期待できますので、検討の余地は大ありです。

注意点としてはExcel上のシート作業があるため、その際のデータの欠損に注意が必要です。

シートが自動的に値を認識してデータをいじってしまう可能性の考慮のことです。

例えばセルに値を「0123456789」と入力してEnterを押下すると「123456789」と数値扱いされ、勝手に頭文字のゼロが欠損したりする挙動のことです。

扱うデータに合わせた対策が必要になる点も押さえておきましょう。

結果は以下のとおりです。
Excelのソートメソッドを使ったソート結果

Wizhookオブジェクトを使ってソートする

もしAccess上でVBAを使用している場合に恩恵が得られるマニアックなWizhookオブジェクトを使ったソートのご紹介です。

Sub Sample_005()

    Dim objDic As New Dictionary
    Dim arrKeys() As String
    Dim arrSort As Variant
    Dim varTMP As Variant
    Dim i As Integer
  
    'Dictionary(連想配列)にデータを格納
    objDic.Add "日本", "Japan"
    objDic.Add "アメリカ", "America"
    objDic.Add "中国", "China"
    objDic.Add "イギリス", "England"
    objDic.Add "ドイツ", "Germany"
    objDic.Add "オーストラリア", "Australia"
    objDic.Add "カナダ", "Canada"
    
    i = 0
    ReDim arrKeys(objDic.Count - 1)
    
    'Dictionary(連想配列)キーを1次元配列化
    For Each varTMP In objDic.Keys
        'ソート前のキーとデータをデバッグ出力
        Debug.Print varTMP & ":" & objDic.Item(varTMP)
        '1次元配列に格納
        arrKeys(i) = varTMP
        
        i = i + 1
    Next
    
    'WizHookを有効化
    WizHook.Key = 51488399
    
    '配列内ソート
    WizHook.SortStringArray arrKeys
        
    Debug.Print "====================="
    
    For i = LBound(arrKeys) To UBound(arrKeys)
        'データを抽出
        varTMP = objDic.Item(arrKeys(i))
        'もともと格納されているペアを削除
        objDic.Remove arrKeys(i)
        '新規ペアとして追加
        objDic.Add arrKeys(i), varTMP
    Next
    
    'Dictionary(連想配列)キーを1次元配列化
    arrSort = objDic.Keys
     
    'ソート結果の出力
    For i = LBound(arrSort) To UBound(arrSort)
        'キーとデータをデバッグ出力
        Debug.Print arrSort(i) & ":" & objDic.Item(arrSort(i))
    Next
 
    'Dictionary(連想配列)を開放
    Set objDic = Nothing
   
End Sub

この処理の補足

Wizhookオブジェクトとは知る人ぞ知っているマニアックなAccess VBAの隠しオブジェクトの一つにあたりますが、メソッドに配列内ソートをサポートしています。

ソートの速度も高速な上に膨大なデータ量の配列もダイレクトにソートしてくれます。

ただし以下の条件があります。

  • 一次元配列であること
  • 配列のデータ型が文字列であること

Wizhookオブジェクトでソートする上でのメソッドの仕様です。

ExcelVBAではサポートされていないため Access 限定の手法になりますが、参照設定や CreateObject などでバインディングする必要もないので、気軽に使用することができます。

SortStringArray メソッド以外にも面白いプロパティやメソッドをサポートしているため、興味のある方はぜひ検討の一つに加えてみてはいかがでしょうか。

結果は以下のとおりです。
Wizhookオブジェクトのソートメソッドを使ったソート結果

Dictionary(連想配列)のソートのまとめ

いろいろDictionary(連想配列)のソートを実現するための手法について考えてみましたが、いかがだったでしょうか。

Dictionary(連想配列)オブジェクトとしてはソートをサポートしておりませんが、いろいろな切り口でソートを実現していくことが可能なサンプルをご紹介できたのではないかなと思います。

プログラム作成の環境や条件などで採用できない手法もあるかもしれませんが、手法も数多く知っておくことで対処可能なケースがほとんどですので、挙げたサンプルを参考にご活用いただければ幸いです。

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