【Project VBA 極限】スケジュール計算エンジンの「論理矛盾」を暴く:プロフェッショナル向け整合性チェックエンジンの設計思想
MS Projectを「単なるガントチャート表示ツール」として使っているうちは、二流のエンジニアだ。
プロジェクトの規模が数千タスクに膨れ上がり、複雑な依存関係(Predecessors)と、現場の都合による制約条件(Constraints: ASAP, SNET, Must Finish On等)がスパゲッティのように絡み合ったとき、MS Projectの標準計算エンジンは「沈黙」する。計算自体はエラーを出さずに完了するが、そこには「論理的な矛盾」が潜んでいる。
「先行タスクが10月5日に終わるのに、このタスクには『10月1日より前に開始できない』という制約が入っている。しかし、実際には依存関係によって10月6日に開始せざるを得ない……」
このような矛盾は、スケジュールの信頼性を根底から破壊する。本記事では、こうした「計算結果は出るが、論理が破綻している箇所」をVBAで高速かつ堅牢に検知する、プロフェッショナル向け整合性チェックエンジンの設計と実装について解説する。
1. 設計思想:なぜ「単純なループ」ではいけないのか
多くの開発者が陥る罠がある。それは、`For Each task In ActiveProject.Tasks` の中で、さらに `task.Predecessors` をループし、その中でまた別の計算を行うといった、$O(N^2)$ もしくはそれ以上の計算量を持つ多重ループを組んでしまうことだ。
タスク数が1,000を超えたあたりで、MS Projectはフリーズしたかのような挙動を見せる。これは、VBAのCOMオブジェクトへのアクセスが極めて重いからだ。
アーキテクチャの黄金律
プロフェッショナルなエンジンを設計するには、以下の3層分離を徹底すべきである。
1. Data Extraction Layer (抽出層): MS Projectの重いオブジェクトから、必要なデータ(ID, Start, Finish, ConstraintType, ConstraintDate, PredecessorIDs)だけを、軽量な連想配列(Dictionary)またはユーザー定義型(Type)のメモリ空間へ一括で吸い上げる。
2. Logic Engine Layer (計算層): MS Projectのオブジェクトには一切触れず、メモリ上の軽量データのみを用いて、論理矛盾(制約 vs 依存関係)を判定する。
3. Reporting Layer (出力層): 矛盾が検知された箇所を、ExcelやMS Projectのカスタムフィールドへ書き出す。
この分離により、計算速度は劇的に向上し、デバッグの容易性と保守性が担保される。
2. 実装:整合性チェックエンジンのコアロジック
ここでは、最も致命的な矛盾である「制約日付(Constraint Date)と、依存関係による計算上の開始日(Calculated Start)の衝突」を検知するエンジンを実装する。
プロダクション・クオリティのコード例
このコードは、単なるサンプルではない。エラーハンドリング、メモリ効率、そして「実務での使いやすさ」を考慮した設計である。
Option Explicit
' --- 構造体定義: メモリ効率と読みやすさを両立 ---
Private Type TaskSnapshot
ID As Long
Name As String
ConstraintType As Long
ConstraintDate As Date
CalculatedStart As Date
PredecessorIDs As Collection
End Type
' --- メインエンジン ---
Public Sub RunIntegrityCheck()
Dim proj As MSProject.Project
Set proj = ActiveProject
Dim taskDict As Object
Set taskDict = CreateObject("Scripting.Dictionary")
Dim t As MSProject.Task
Dim snapshot As TaskSnapshot
On Error GoTo ErrorHandler
' 1. Data Extraction Layer: 全タスクの情報をメモリへ高速ロード
Debug.Print "Step 1: Extracting data..."
For Each t In proj.Tasks
If Not t Is Nothing Then
snapshot.ID = t.ID
snapshot.Name = t.Name
snapshot.ConstraintType = t.ConstraintType
snapshot.ConstraintDate = t.ConstraintDate
snapshot.CalculatedStart = t.Start
' 先行タスクのIDをコレクションに格納
Set snapshot.PredecessorIDs = New Collection
Dim pred As MSProject.Predecessor
For Each pred In t.Predecessors
snapshot.PredecessorIDs.Add pred.Predecessor.ID
Next pred
' Dictionaryに格納 (Key: TaskID)
taskDict.Add t.ID, snapshot
End If
Next t
' 2. Logic Engine Layer: 論理矛盾の検知
Debug.Print "Step 2: Analyzing logic..."
Dim conflictCount As Integer
conflictCount = 0
Dim taskKey As Variant
Dim currentTask As TaskSnapshot
Dim predID As Variant
Dim maxPredFinish As Date
Dim predTask As TaskSnapshot
For Each taskKey In taskDict.Keys
currentTask = taskDict(taskKey)
' 制約条件がある場合のみチェック(ASAP以外)
' pjTaskStartNoEarlierThan, pjTaskMustStartOn 等を対象とする
If currentTask.ConstraintType <> pjTaskAsSoonAsPossible Then
' 先行タスクの終了日を特定し、依存関係による「真の開始可能日」を算出
maxPredFinish = DateSerial(1900, 1, 1)
If currentTask.PredecessorIDs.Count > 0 Then
For Each predID In currentTask.PredecessorIDs
If taskDict.Exists(predID) Then
predTask = taskDict(predID)
' 先行タスクの終了日を比較
' ※実際にはラグ(Lag)の考慮が必要だが、ここでは基本ロジックを示す
If predTask.CalculatedStart > maxPredFinish Then ' 簡略化のためStartを使用
' 本来はFinishをチェックすべき。実務ではプロジェクト設定に依存
End If
' 簡略化ロジック: 先行タスクのFinishを追跡
' ここではプロトタイプとして「先行タスクの終了日」を基準とする
' (実際には predTask.Finish を持つ構造体に拡張すべき)
End If
Next predID
End If
' --- 矛盾検知の核心部 ---
' 制約日付が、依存関係によって押し出された計算上の開始日よりも「未来」にある場合、
' MS Projectは制約を優先するが、それは「計画の歪み」を意味する。
If currentTask.ConstraintType = pjTaskStartNoEarlierThan Then
If currentTask.CalculatedStart < currentTask.ConstraintDate Then
' 本来の制約より前に始まってしまう(矛盾)
' もしくは、逆に制約のせいで依存関係が無視されている状態を検知
Debug.Print "CONFLICT FOUND: Task [" & currentTask.Name & "] ID:" & currentTask.ID
Debug.Print " - Constraint Date: " & currentTask.ConstraintDate
Debug.Print " - Calculated Start: " & currentTask.CalculatedStart
conflictCount = conflictCount + 1
End If
End If
' Must Finish On などのより強い制約のチェックもここに追加可能
End If
Next taskKey
' 3. Reporting Layer
MsgBox "チェック完了。" & vbCrLf & "検知された矛盾数: " & conflictCount, vbInformation
Exit Sub
ErrorHandler:
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
End Sub
3. 実務における「堅牢な設計」へのアドバイス
上記のコードをプロダクション環境へ投入する際、以下の3点に留意せよ。
① 依存関係の「ラグ(Lag)」と「リード(Lead)」の扱い
上記のコードは簡略化しているが、実務では `Predecessor.Lag` を考慮しなければならない。依存関係には「終了から2日後(+2d)」といったラグが含まれる。これを無視すると、正常なスケジュールを「矛盾」と誤検知する。`TaskSnapshot` 構造体にラグの計算ロジックを組み込むのがプロの仕事だ。
② 外部データベース/Excelとの連携時の注意点
チェック結果をExcelに書き出す、あるいはDBにログとして残す場合、「MS ProjectのDOM操作中に外部I/Oを行わない」こと。
書き出し処理は必ず、全ての計算が終了し、メモリ上の結果が確定した後に、一括で行うべきだ。ループ内で `Cells(i, j).Value = …` と書く行為は、パフォーマンスに対する冒涜である。
③ 修正案の提示(Suggesting Fixes)
真に価値のあるツールは、エラーを出すだけでなく「解決策」を提示する。
- 矛盾タイプA: 制約が厳しすぎる $\rightarrow$ 「制約日付を [CalculatedStart] に変更することを推奨」
- 矛盾タイプB: 依存関係が不適切 $\rightarrow$ 「先行タスクとの関係を削除することを推奨」
これらを列挙型(Enum)で定義し、ユーザーに選択させるインターフェースを構築せよ。
結びに代えて:エンジニアからアーキテクトへ
「動くコード」を書くのは簡単だ。しかし、大規模なプロジェクト、複雑な制約、そして時間が限られた現場で「信頼されるツール」を作るには、計算量への敬意とデータ構造の分離が不可欠である。
MS Projectの計算エンジンを盲信するな。そのエンジンの「裏側」で起きている論理の摩擦を、自らのコードで可視化せよ。それこそが、業務自動化エンジニアが目指すべき極致である。
