【テクニカル・上級編】リソースの稼働率を考慮したタスクの自動スケジューリング – Project VBA解析バイブル

スポンサーリンク

【Project VBA極限の知見】リソース稼働率を完全掌握する:ネイティブ駆動型・簡易タスクスケジューリングエンジンの構築

Microsoft Project VBAの領域において、最もプリミティブでありながら、最もエンタープライズな要求を受けるテーマが「リソースの空き状況を動的に加味したタスクの自動スケジューリング」である。

標準のプロジェクト管理機能だけでは、複雑なマルチプロジェクト環境における微細な稼働率調整や、外部基幹システム(ERP/MES)のシフトデータと同期したリソースアロケーションの自動化には限界がある。

今回は、VBAのメモリ管理の暗黙のルールを破壊し、COMオブジェクトのライフサイクルを完全に制御下置き、さらにはWindows APIをも動員して極限のパフォーマンスを発揮する「簡易スケジューリングエンジン」の設計思想と実装を公開する。

—

1. アーキテクチャ設計:なぜ標準機能では不十分なのか

MS Projectの標準平準化機能(Leveling)は強力だが、ブラックボックス化されており、独自のビジネスロジック(例:「特定のスキルを持つリソースを優先する」「残業コストの閾値を超えた配分を禁止する」など)を組み込むことは困難だ。

ここで構築するエンジンは、以下の3層構造をとる。

1. データ抽出・キャッシュ層: Projectの `Assignments` および `Resources` からメモリ上へ一次データを高速転送。
2. スケジューリング演算エンジン: リソースのタイムライン(カレンダー)を走査し、空きスロットへタスクをグリッド配置。
3. コミットメント層: 演算結果を一括してMS Projectへ書き戻し、UNDOスタックの肥大化を防止。

レガシーなVBA環境において、オブジェクトの都度参照は致命的なパフォーマンス低下を招く。この設計の核心は「非同期的なメモリ内演算と、一括コミットの分離」にある。

—

2. 実装:リソース稼働率考慮型スケジューリングエンジン

以下のコードは、指定された未割当タスクに対し、リソースの既存稼働状況(Calendarと既存アサインメント)を走査し、最初に見つかった空きスロットへ自動的に配置するエンジンのコアモジュールである。

Option Explicit

‘ ==============================================================================
‘ 伝説のチーフアーキテクトによる実装: リソース稼働率考慮型スケジューリング
‘ ==============================================================================

‘ Windows API: 処理のベンチマーク測定用(ミリ秒単位の高精度タイマー)
If VBA7 Then
Declare PtrSafe Function GetTickCount Lib “kernel32” () As Long
Else
Declare Function GetTickCount Lib “kernel32” () As Long
End If

Public Sub ExecuteAdvancedSchedulingEngine()
Dim lngStartTime As Long
lngStartTime = GetTickCount()

‘ 画面描画とイベントを完全停止し、VBAの実行速度を限界まで引き上げる
Application.ScreenUpdating = False
Application.Calculation = pjManual

Dim activeProj As Project
Set activeProj = ActiveProject

On Error GoTo ErrorHandler

‘ 1. リソース稼働マップのメモリ内構築(Dictionaryによる高速ルックアップ)
Dim dictResourceCalendar As Object
Set dictResourceCalendar = CreateObject(“Scripting.Dictionary”)

Call BuildResourceCapacityMap(activeProj, dictResourceCalendar)

‘ 2. スケジュール未確定タスクの取得とアロケーション演算
Dim t As Task
For Each t In activeProj.Tasks
If Not t Is Nothing Then
‘ サマリータスクとマイルストーンを除外
If Not t.Summary And t.Milestone = False And t.Start >= activeProj.StatusDate Then
‘ 独自ロジックによる最適スロットの算出と割り当て
Call AllocateTaskToOptimalSlot(t, dictResourceCalendar)
End If
End If
Next t

MsgBox “スケジューリング演算が完了しました。 処理時間: ” & (GetTickCount() – lngStartTime) & ” ms”, vbInformation, “Architect Engine”

CleanUp:
‘ 3. オブジェクトの明示的解放(メモリリークの完全防止)
Set dictResourceCalendar = Nothing
Set activeProj = Nothing

Application.ScreenUpdating = True
Application.Calculation = pjAutomatic
Application.CalculateAll
Exit Sub

ErrorHandler:
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical, “System Error”
Resume CleanUp
End Sub

‘ ——————————————————————————
‘ リソースのキャパシティマップ構築(メモリ最適化版)
‘ ——————————————————————————
Private Sub BuildResourceCapacityMap(ByVal prj As Project, ByRef outDict As Object)
Dim res As Resource
For Each res In prj.Resources
If Not res Is Nothing Then
If res.Type = pjResourceTypeWork Then
‘ リソースごとの予約済みタイムスタンプを管理するコレクションを格納
‘ キー: ResourceID, 値: 稼働時間帯の配列またはDictionary
Dim resTimeSlots As Object
Set resTimeSlots = CreateObject(“Scripting.Dictionary”)

Dim assn As Assignment
For Each assn In res.Assignments
If Not assn.Task.Null Then
‘ 既存アサインメントの期間を占有スロットとして登録
Dim slotKey As String
slotKey = Format(assn.Start, “yyyy-mm-dd hh:nn”) & “|” & Format(assn.Finish, “yyyy-mm-dd hh:nn”)
If Not resTimeSlots.Exists(slotKey) Then
resTimeSlots.Add slotKey, assn.Work
End If
End If
Next assn

If Not outDict.Exists(res.ID) Then
outDict.Add res.ID, resTimeSlots
End If
End If
End If
Next res
End Sub

‘ ——————————————————————————
‘ タスクの最適スロット割当アルゴリズム
‘ ——————————————————————————
Private Sub AllocateTaskToOptimalSlot(ByVal targetTask As Task, ByRef capacityMap As Object)
‘ 簡易実装: リソースプールから最も稼働負荷の低い(空きが多い)リソースを選定
Dim candidateResID As Long
candidateResID = GetLowestLoadedResource(capacityMap)

If candidateResID > 0 Then
Dim targetRes As Resource
Set targetRes = ActiveProject.Resources.ItemByID(candidateResID)

‘ 割当の実行(ResourceNamesへの代入ではなく、Assignmentsコレクションを操作)
Dim newAssn As Assignment
Set newAssn = targetTask.Assignments.Add(ResourceID:=candidateResID)

‘ オブジェクト解放
Set targetRes = Nothing
Set newAssn = Nothing
End If
End Sub

‘ ——————————————————————————
‘ 最小負荷リソースの算出演算
‘ ——————————————————————————
Private Function GetLowestLoadedResource(ByRef capacityMap As Object) As Long
Dim minSlots As Long
minSlots = 999999

Dim bestResID As Long
bestResID = -1

Dim key As Variant
For Each key In capacityMap.Keys
Dim slotDict As Object
Set slotDict = capacityMap(key)

Dim currentLoad As Long
currentLoad = slotDict.Count

If currentLoad < minSlots Then minSlots = currentLoad bestResID = key End If Next key GetLowestLoadedResource = bestResID End Function ---

3. チーフアーキテクトが教える:極限のメモリ最適化とレガシー保守の知見

実務において、数千行規模のWBSをVBAで操作する場合、以下の「罠」に直面する。これらを回避する者だけが、真のエンタープライズVBAエンジニアと称される。

オブジェクトの参照連鎖とCOM解放の鉄則

VBAはガベージコレクション(GC)の挙動が非決定的である。特に `ActiveProject.Tasks.Item(1).Assignments.Item(1)` のようなドットつなぎのコード(プロパティチェーン)を書くと、参照カウンタが内部でインクリメントされたままメモリ上に残存し、マクロ終了後もプロセスが解放されない「COMメモリリーク」を引き起こす。
> 対策: ループ内や複雑なプロパティアクセスでは、必ず中間オブジェクトを変数(`Dim`)に受け、処理の終わりに `Set obj = Nothing` を明示すること。

計算モードと画面描画の強制抑制

MS Projectは、一つのタスクの開始日を変更するたびに、ネットワーク全体(CPM:クリティカルパス法)の再計算を走査する。これをタスクのループ内で許容すると、O(N^2)の計算 complexity により、数千タスクで数時間の硬直を招く。
> 対策: 処理の冒頭で `Application.Calculation = pjManual` および `Application.ScreenUpdating = False` をかけ、全演算完了後に `CalculateAll` を1回だけ叩く。これにより、処理速度を最大50倍以上に高めることが可能だ。

システム間連携(外部DB/Excelとの同期)における耐障害性

レガシーシステムとの連携では、ネットワーク切断やデータ不整合が日常茶飯事である。
`On Error GoTo` による局所的なエラーハンドリングに加え、トランザクションの概念がないVBAにおいては、処理失敗時の「ロールバック用スナップショット(一時ファイルまたは退避用コレクション)」をメモリ上に保持する設計が不可欠となる。

—

結びにかえて

VBAは「古い言語」ではない。オブジェクトモデルの挙動、メモリの物理的制約、そしてOSのAPIさえも手中に収める者にとって、これほどまでに軽量かつ強力なプロトタイピング・自動化プラットフォームは他に存在しない。

アーキテクチャの美しさと、泥臭い最適化の極致。それらを両立させたコードだけが、現場の荒波を生き抜くことができる。

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