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(連想配列)のソートはキーを基準に格納されているペアを並べ替える処理になります。
クイックソートは回帰ループ処理の代表格のようなベーシックなロジックですが、速度も速いし、理解を進めながら組み込んでいくと達成感が湧いてくるロジックです。(私の場合ですが)
結果は以下のとおりです。
ソートの並び順(昇順、降順)は再取り込みの際のループの回し方で調整することができます。
クイックソートはソートアルゴリズムの中では早い方ですが、データ量によりやや処理速度にぶれがあるので、対象データに合わせて採用するか否か検討する必要があります。
正直言えばこれでほとんど問題のないレベルかもしれません。
.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で動的配列を処理しているとReDimやPreserveなどで配列のサイズを変更していく必要があるのですが、このArrayListはDictionary(連想配列)やCollectionなどのようにAddメソッドでデータを追加していくことが可能です。
さらにSortメソッドやArrayListから配列へ変換するメソッドもサポートされています。
上記サンプルのようにArrayListを活用することでソート部分を解決させることができるというわけです。
※オートメーションエラーについては、ArrayListの紹介記事で詳しく取り上げたいと思います。
処理の結果は以下のとおりです。
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」と数値扱いされ、勝手に頭文字のゼロが欠損したりする挙動のことです。
扱うデータに合わせた対策が必要になる点も押さえておきましょう。
結果は以下のとおりです。
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 メソッド以外にも面白いプロパティやメソッドをサポートしているため、興味のある方はぜひ検討の一つに加えてみてはいかがでしょうか。
結果は以下のとおりです。
Dictionary(連想配列)のソートのまとめ
いろいろDictionary(連想配列)のソートを実現するための手法について考えてみましたが、いかがだったでしょうか。
Dictionary(連想配列)オブジェクトとしてはソートをサポートしておりませんが、いろいろな切り口でソートを実現していくことが可能なサンプルをご紹介できたのではないかなと思います。
プログラム作成の環境や条件などで採用できない手法もあるかもしれませんが、手法も数多く知っておくことで対処可能なケースがほとんどですので、挙げたサンプルを参考にご活用いただければ幸いです。