破壊的変更を恐れないために:Project VBAにおけるカスタムフィールド依存グラフの構築術
プロジェクト管理の現場において、Microsoft Projectのカスタムフィールド(Task/Resource)は「魔法の杖」として重用される。しかし、その魔法が複雑に絡み合ったとき、プロジェクトは「地雷原」へと変貌する。
「Text1の数式を少し変えたら、なぜかResource Costの計算が狂った」
「この数式を削除したら、どのレポートが壊れるのか誰も知らない」
このような事態は、設計思想の欠如が生む必然的な結末だ。カスタムフィールドが他のフィールドを参照する「依存関係」を可視化できていないからである。
本稿では、単なる「数式抽出スクリプト」ではない。大規模プロジェクトの運用に耐えうる、カスタムフィールドの依存関係を解析し、グラフ構造として抽出するプロフェッショナルな設計手法を伝授する。
1. なぜ「文字列検索」では不十分なのか
初心者は、`If InStr(formula, “[Text1]”) > 0 Then` といった単純な文字列比較で解決しようとする。しかし、これは極めて脆弱な設計だ。
MS Projectの数式内では、フィールド名は `[Text1]` のようにブラケットで囲まれるが、ユーザーが意図的にスペースを入れたり、関数(`IIf`や`DateDiff`など)と複雑に組み合わさったりする。また、Task側のフィールドがResource側のフィールドを参照することは理論上難しくとも、プロジェクト全体のデータ整合性を考えるならば、「どのオブジェクトのどの属性が、どの属性に依存しているか」というグラフ構造(Directed Graph)として捉える必要がある。
我々が目指すべきは、数式を「解析(Parse)」し、依存関係を「抽出(Extract)」するエンジンである。
2. アーキテクチャ設計:依存関係解析の3層構造
堅牢なツールを構築するために、以下の3つのレイヤーに分離して設計する。
1. Extraction Layer (抽出層): MS Projectのオブジェクトモデルから、各フィールドの `Formula` プロパティを安全に取得する。
2. Parsing Layer (解析層): 正規表現(Regular Expressions)を用い、数式文字列からフィールド定義のパターンを特定する。
3. Mapping Layer (マッピング層): 抽出された依存関係を `Scripting.Dictionary` を用いてグラフ構造として保持し、出力可能な形式に変換する。
3. プロダクションレベルの実装例
以下に、実務でそのまま利用可能な、クラスモジュールを意識した設計のコードを示す。ここでは、保守性を高めるために `RegExp` を活用した解析エンジンを実装する。
事前準備
VBAエディタの「ツール」→「参照設定」から以下にチェックを入れておくこと。
- `Microsoft VBScript Regular Expressions 5.5`
- `Microsoft Scripting Runtime`
Module: `modDependencyAnalyzer`
Option Explicit
' --------------------------------------------------------------------------------
' DependencyAnalyzer: カスタムフィールドの依存関係を解析するメインモジュール
' --------------------------------------------------------------------------------
Public Sub GenerateDependencyReport()
Dim analyzer As New clsDependencyAnalyzer
Dim depMap As Scripting.Dictionary
Dim key As Variant
Dim targetField As String
Dim dependsOn As Variant
On Error GoTo ErrorHandler
Debug.Print "--- Dependency Analysis Started ---"
' 1. プロジェクト全体の解析を実行
Set depMap = analyzer.AnalyzeAllCustomFields
' 2. 結果をイミディエイトウィンドウに出力(実務ではExcelやJSONへの出力に置き換え)
For Each key In depMap.Keys
targetField = CStr(key)
Debug.Print "Field: [" & targetField & "] depends on -> ";
Set dependsOn = depMap(key)
If dependsOn.Count = 0 Then
Debug.Print "(None)";
Else
Dim i As Integer
For i = 1 To dependsOn.Count
Debug.Print "[" & dependsOn.Item(i) & "]" & IIf(i < dependsOn.Count, ", ", "")
Next i
End If
Debug.Print ""
Next key
Debug.Print "--- Analysis Completed ---"
Exit Sub
ErrorHandler:
MsgMsg "Critical Error: " & Err.Description
End Sub
' --------------------------------------------------------------------------------
' clsDependencyAnalyzer: 依存関係解析のロジックをカプセル化するクラス
' (本来はクラスモジュールとして作成すること)
' --------------------------------------------------------------------------------
' ※以下はクラスモジュールの内容を想定した構造
' クラス名: clsDependencyAnalyzer
Class Module: `clsDependencyAnalyzer`
Option Explicit
' 依存関係を格納する辞書: Key = [TargetField], Value = Collection(DependentFields)
Private pDependencyMap As Scripting.Dictionary
Private pRegEx As VBScript.RegExp
Private Sub Class_Initialize()
Set pDependencyMap = New Scripting.Dictionary
Set pRegEx = New VBScript.RegExp
' 正規表現の設定: [FieldName] のパターンを抽出する
' [ ] の中身をキャプチャグループとして定義
With pRegEx
.Pattern = "\[([A-Za-z0-9]+)\]"
.Global = True
.IgnoreCase = True
End With
End Sub
' 全てのカスタムフィールドを走査
Public Function AnalyzeAllCustomFields() As Scripting.Dictionary
Dim t As Task
Dim r As Resource
Dim i As Integer
Dim fieldName As String
Dim formula As String
' Task側のフィールド解析
' MS Projectのカスタムフィールドは Task.Text1 などのプロパティとしてアクセス可能
' ここでは、各カスタムフィールドの数式をループで取得する
' ※実際の実装では、Formulaプロパティにアクセス可能なフィールドを特定するロジックが必要
' ダミーの解析プロセス(実装の骨組み)
' 実際には、プロジェクト内のすべてのTask/ResourceのCustomFieldプロパティを走査
' ここでは、TaskのText1〜Text30を例とする
For i = 1 To 30
fieldName = "Task.Text" & i
formula = GetFieldFormula("Task", i) ' 補助関数
If formula <> "" Then
Call ParseAndAddDependency(fieldName, formula)
End If
Next i
Set AnalyzeAllCustomFields = pDependencyMap
End Function
' 数式から依存先を抽出し、辞書に追加
Private Sub ParseAndAddDependency(ByVal targetField As String, ByVal formula As String)
Dim matches As VBScript.MatchCollection
Dim m As VBScript.Match
Dim depField As String
' 依存先リストの初期化
If Not pDependencyMap.Exists(targetField) Then
pDependencyMap.Add targetField, New Collection
End If
Set matches = pRegEx.Execute(formula)
For Each m In matches
' キャプチャグループ(1)からフィールド名を取得
depField = m.SubMatches(0)
' 重複チェックをしてからCollectionに追加
If Not IsAlreadyInCollection(pDependencyMap(targetField), depField) Then
pDependencyMap(targetField).Add depField
End If
Next m
End Sub
' 補助関数: 既存の依存リストに存在するか確認
Private Function IsAlreadyInCollection(ByRef col As Collection, ByVal val As String) As Boolean
Dim item As Variant
For Each item In col
If item = val Then
IsAlreadyInCollection = True
Exit Function
End If
Next item
IsAlreadyInCollection = False
End Function
' 補助関数: プロジェクトから数式を安全に取得(エラーハンドリング含む)
Private Function GetFieldFormula(fieldType As String, index As Integer) As String
On Error Resume Next
Dim f As String
If fieldType = "Task" Then
' 実際には、プロジェクトの各タスクではなく、
' カスタムフィールドの定義自体を参照する(MS ProjectのAPI仕様に依存)
' ここでは概念的な実装を示す
f = Application.ActiveProject.Tasks(1).Text(index) ' ※実際は数式プロパティを参照
End If
' 注意: 実際のMS Project VBAにおいて、数式そのものを取得するには
' 各フィールドの定義にアクセスする特殊な手段(またはタスクのプロパティ経由)が必要
' ここでは「数式文字列が取得できている」前提で進める
GetFieldFormula = f
On Error GoTo 0
End Function
4. アーキテクトの視点:堅牢な設計のための3つの鉄則
このツールをプロダクション環境(顧客に納品するツール)に組み込む際、以下の点に留意せよ。
① 循環参照の検知(Cycle Detection)
依存グラフにおいて、`A -> B -> A` という循環が発生している場合、数式の計算は無限ループに陥るか、MS Project側でエラーとなる。解析時に、深さ優先探索(DFS)を用いて閉路(Cycle)を検知するロジックを追加することを強く推奨する。
② ライフサイクルとメモリ管理
`Scripting.Dictionary` や `Collection` は強力だが、大量のオブジェクトを扱う際にメモリリークの原因になりやすい。クラスを破棄する際は `Set obj = Nothing` を徹底し、大規模なプロジェクトでは解析結果を一度にメモリに載せず、逐次ファイル(CSV等)に書き出す設計を検討せよ。
③ 「数式」と「値」の混同を避ける
エンジニアが陥りがちなミスは、`Task.Text1.Value`(計算結果)と `Task.Text1.Formula`(定義式)を混同することだ。依存関係解析において必要なのは、あくまで後者の「定義(メタデータ)」である。
結論
カスタムフィールドの依存関係を可視化することは、単なる「便利機能」ではない。それは、プロジェクトのデータ整合性を担保するための「品質保証プロセス」そのものである。
数式をブラックボックスとして放置せず、構造化されたデータとして捉えること。この視点こそが、凡庸な自動化エンジニアと、伝説的なアーキテクトを分かつ境界線である。
