目次
はじめに
前回の記事では、辞書(Dictionary)で単純な数値をカウントする際にIf~Exists~Addが不要になる便利な仕様を紹介しました。しかし、辞書の値として配列を使う場合は、この便利な仕様が使えません。この記事では、配列を扱う際の正しい書き方を解説します。
辞書に配列を格納する場面
商品ごとに複数の情報(例えば、1月・2月・3月の売上)を管理したい場合、辞書の値として配列を使うことがあります。
' 商品Aの3ヶ月分の売上を配列で管理
dict("商品A") = Array(100, 200, 150)
このように、1つのキーに対して複数の値をまとめて持たせたい場合に配列が便利です。
配列では自動作成が使えない
単純な数値の場合は以下のように書けましたが、配列の場合は同じ方法が使えません。
' 数値の場合(これはOK)
dict("商品A") = dict("商品A") + 1
' 配列の場合(これはエラーになる)
dict("商品A")(0) = dict("商品A")(0) + 100
配列の要素に直接アクセスしようとすると、キーが存在しない場合にエラーが発生してしまいます。辞書の自動作成機能は、配列の要素レベルまでは面倒を見てくれないためです。
Exists を使った正しい書き方
配列を扱う場合は、Existsで存在確認をしてから処理する必要があります。
基本的な流れは以下の通りです。
-
Existsでキーの存在を確認 - 存在しない場合は配列を新規作成して
Add - 存在する場合は配列を取り出して値を更新し、書き戻す
If Not dict.Exists("商品A") Then
' 新規作成
Dim newArr(1 To 3) As Long
newArr(1) = 100: newArr(2) = 200: newArr(3) = 150
dict.Add "商品A", newArr
Else
' 既存の配列を更新
Dim tmp As Variant
tmp = dict("商品A")
tmp(1) = tmp(1) + 100
dict("商品A") = tmp
End If
ここで重要なのは、辞書から配列を一度取り出して、変更してから書き戻すという手順です。dict("商品A")(1) = ...のように直接変更することはできません。
配列の場合は一時変数(tmp)を使って「取り出し→変更→書き戻し」という手順が必要になります。これは辞書の仕様上の制約です。
汎用的な関数として使う
配列の登録・更新処理を毎回書くのは大変なので、関数として汎用化しておくと便利です。以下のような関数を作成しておけば、どこからでも呼び出せます。
' 辞書に対して新規登録または更新を行う関数
' 引数: 辞書、キー、配列
Public Sub UpdateDict(ByRef dict As Scripting.Dictionary, _
ByVal key As String, ByVal arr As Variant)
' キー存在確認
If Not dict.Exists(key) Then
' 新規登録
dict.Add key, arr
Else
' 既存 -> 配列全体を加算
Dim i As Long
Dim tmp As Variant: tmp = dict(key)
For i = LBound(arr) To UBound(arr)
tmp(i) = tmp(i) + arr(i)
Next i
dict(key) = tmp
End If
End Sub
この関数の特徴は、LBoundとUBoundを使って配列の開始位置と終了位置を自動判定している点です。配列が(1 To 3)でも(0 To 2)でも自動的に対応できます。
【関数の使い方】
Sub TestUpdateDictFunction()
Dim dict As New Scripting.Dictionary
Dim sales(1 To 3) As Long
' 商品Aの売上データ
sales(1) = 100: sales(2) = 200: sales(3) = 150
UpdateDict dict, "商品A", sales
' 商品Aの追加売上
sales(1) = 50: sales(2) = 80: sales(3) = 30
UpdateDict dict, "商品A", sales
' 結果確認
Dim result As Variant: result = dict("商品A")
Debug.Print result(1), result(2), result(3) ' 150, 280, 180
End Sub
関数化することで、コードの見通しが良くなり、同じ処理を繰り返し書く必要がなくなります。
LBoundは配列の最小インデックス、UBoundは配列の最大インデックスを返す関数です。これらを使うことで、配列のサイズに関係なく柔軟に対応できます。
実際に試してみる
実際に動作を確認できるコードを用意しました。空白のExcelブックを開き、以下の手順で試してみてください。
- 新しいExcelブックを開く
-
Alt + F11でVBエディタを開く - 「ツール」→「参照設定」から「Microsoft Scripting Runtime」にチェックを入れる
- 「挿入」→「標準モジュール」で新しいモジュールを追加
- 以下のコードを貼り付けて実行
辞書を使うには参照設定が必要です。設定方法は以下の記事で解説しています。
【基本例】配列の新規登録と更新
Sub TestDictionaryWithArray()
' 辞書を作成
Dim dict As New Scripting.Dictionary
Dim sales(1 To 3) As Long
' 商品Aの売上データ(1月、2月、3月)
sales(1) = 100: sales(2) = 200: sales(3) = 150
UpdateDict dict, "商品A", sales
' 商品Aの追加売上
sales(1) = 50: sales(2) = 80: sales(3) = 30
UpdateDict dict, "商品A", sales
' 結果を表示
Dim result As Variant
result = dict("商品A")
Dim msg As String
msg = "商品Aの売上集計結果" & vbCrLf & vbCrLf
msg = msg & "1月: " & result(1) & "円" & vbCrLf
msg = msg & "2月: " & result(2) & "円" & vbCrLf
msg = msg & "3月: " & result(3) & "円"
MsgBox msg, vbInformation
End Sub
' 辞書に対して新規登録または更新を行う関数
Public Sub UpdateDict(ByRef dict As Scripting.Dictionary, _
ByVal key As String, ByVal arr As Variant)
If Not dict.Exists(key) Then
dict.Add key, arr
Else
Dim i As Long
Dim tmp As Variant: tmp = dict(key)
For i = LBound(arr) To UBound(arr)
tmp(i) = tmp(i) + arr(i)
Next i
dict(key) = tmp
End If
End Sub
実行すると、以下の結果が表示されます。
商品Aの売上集計結果
1月: 150円
2月: 280円
3月: 180円
【応用例】複数商品の売上を集計してシートに出力
より実用的な例として、複数の商品データを集計してシートに出力するコードです。
Sub AggregateProductSales()
' シートをクリア
Cells.Clear
' 辞書を作成
Dim dict As New Scripting.Dictionary
Dim sales(1 To 3) As Long
' ====== データ1回目の登録 ======
' 商品Aのデータ
sales(1) = 100: sales(2) = 200: sales(3) = 150
UpdateDict dict, "商品A", sales
' 商品Bのデータ
sales(1) = 80: sales(2) = 120: sales(3) = 90
UpdateDict dict, "商品B", sales
' ====== データ2回目の登録(加算される) ======
' 商品Aの追加データ
sales(1) = 50: sales(2) = 80: sales(3) = 30
UpdateDict dict, "商品A", sales
' 商品Cのデータ(新規)
sales(1) = 200: sales(2) = 150: sales(3) = 180
UpdateDict dict, "商品C", sales
' 商品Bの追加データ
sales(1) = 40: sales(2) = 60: sales(3) = 50
UpdateDict dict, "商品B", sales
' ====== 結果をシートに出力 ======
' 見出し行
Range("A1").Value = "商品名"
Range("B1").Value = "1月"
Range("C1").Value = "2月"
Range("D1").Value = "3月"
Range("E1").Value = "合計"
' データ行
Dim key As Variant
Dim row As Long: row = 2
For Each key In dict.Keys
Dim arr As Variant
arr = dict(key)
Cells(row, 1).Value = key
Cells(row, 2).Value = arr(1)
Cells(row, 3).Value = arr(2)
Cells(row, 4).Value = arr(3)
Cells(row, 5).Formula = "=SUM(B" & row & ":D" & row & ")"
row = row + 1
Next key
' 列幅を自動調整
Columns("A:E").AutoFit
MsgBox "売上集計が完了しました", vbInformation
End Sub
' 辞書に対して新規登録または更新を行う関数
Public Sub UpdateDict(ByRef dict As Scripting.Dictionary, _
ByVal key As String, ByVal arr As Variant)
If Not dict.Exists(key) Then
dict.Add key, arr
Else
Dim i As Long
Dim tmp As Variant: tmp = dict(key)
For i = LBound(arr) To UBound(arr)
tmp(i) = tmp(i) + arr(i)
Next i
dict(key) = tmp
End If
End Sub
このコードを実行すると、自動的に集計が行われ、以下のような表がシートに出力されます。
| 商品名 | 1月 | 2月 | 3月 | 合計 |
|---|---|---|---|---|
| 商品A | 150 | 280 | 180 | 610 |
| 商品B | 120 | 180 | 140 | 440 |
| 商品C | 200 | 150 | 180 | 530 |
UpdateDict関数を使うことで、配列の登録・更新処理を簡潔に記述できています。同じ関数を何度も呼び出すだけで、自動的に新規登録や加算が行われます。
まとめ
辞書の値として配列を使う場合は、単純な数値のときとは異なり、Existsを使った明示的な存在確認が必要です。配列を更新する際は「取り出し→変更→書き戻し」という手順を踏む必要があります。この処理をUpdateDictのような関数にまとめておくと、繰り返し使う場合に便利で、コードの可読性(読みやすさ)も向上します。