【実務・中級編】計算エンジンをバイパスする:制約条件を無視したタスクの日付強制書き込みの危険性 – Project VBA解析バイブル

スポンサーリンク

こんにちは。チーフアーキテクトの神谷です。

大規模な開発プロジェクトにおいて、Microsoft Project(以下、MS Project)を外部システムやExcelと連携させ、進捗や日程を自動同期するマクロを書いたことがある人は多いはずです。しかし、そのコードをレビューすると、戦慄を覚える実装が後を絶ちません。

‘ 典型的な「アンチパターン」の極み
Task.Start = importedStartDate
Task.Finish = importedEndDate

もしあなたが、あるいはあなたのチームのメンバーがこのようなコードを1行でも書いているなら、即座にそのスクリプトの運用を止めてください。

一見、画面上は指定した日付にタスクが移動し、正常に動いているように見えます。しかし、その瞬間にMS Projectの心臓部である「スケジュール計算エンジン(CPM: クリティカル・パス・メソッド)」は不可逆的に破壊されています。

今回は、なぜ`Task.Start`や`Task.Finish`への直接書き込みがプロジェクトを崩壊へと導くのか、その内部メカニズムを解き明かし、エンタープライズ規模の運用にも耐えうる「制約条件(Constraints)を正しくハンドリングする堅牢なAPI設計」を伝授します。

—

1. なぜ「日付の直接代入」が致命的なのか

ExcelとProjectの決定的な違い

Excelは「セルの値」を保持するツールですが、MS Projectは「タスク間の依存関係、カレンダー、リソース、制約条件から微分方程式のようにスケジュールを導出する計算エンジン」です。

自動スケジュール(Auto Scheduled)タスクにおいて、タスクの日付は「計算結果の出力(Output)」であって、原則として「入力(Input)」ではありません。

暗黙的に発生する「制約条件の強制上書き」

MS ProjectのGUI上でタスクのバーをマウスでドラッグしたとき、何が起きるかご存知でしょうか。タスク情報ダイアログを見ると、制約タイプ(Constraint Type)が勝手に書き換わっているはずです。

VBAで`Task.Start`や`Task.Finish`プロパティに値を直接代入した場合も、内部的にはこれと全く同じ破壊的操作が行われます。

デフォルト設定(ASAP:できるだけ早く)のタスクに対して`Task.Start = “2024/05/10″`と書き込んだ瞬間、MS Projectのエンジンは次のように振る舞います。

1. タスクの`ConstraintType`を強制的に `pjSNET`(Start No Earlier Than:指定日以後に開始) に書き換える。
2. タスクの`ConstraintDate`に代入された日付を設定する。
3. これにより、先行タスクの前倒し完了などによってタスクが早期に着手可能になっても、指定日より前に自動前進しなくなる(=スケジュールの弾力性の喪失)。

`Task.Finish`に代入した場合はさらに悲惨です。`pjFNET`(Finish No Earlier Than) や `pjMFO`(Must Finish On) 等の硬直的な制約が自動付与され、タスクの「期間(Duration)」と「作業時間(Work)」の計算不整合を引き起こし、プロジェクト全体のクリティカルパスを完全に歪曲させます。

これが、「バッチ同期スクリプトを走らせた後、なぜか先行タスクを動かしても後続タスクが連動しなくなった」という怪現象の正体です。

—

2. スケジュール計算エンジンを制御するAPI設計原則

外部DBや進捗管理システムから日程を反映する場合、エンジニアが取るべきアプローチは「目的」によって明確に切り分けなければなりません。

| 実現したい目的 | 誤ったアプローチ | 正しいエンジニアリング・アプローチ |
| :— | :— | :— |
| 外部計画日を絶対値として固定したい | `Task.Start = …`
`Task.Finish = …` | 明示的に `ConstraintType` を `pjMSO`(Must Start On)等に設定し、`ConstraintDate` を与える。 |
| 実績進捗(着手・完了)を反映したい | `Task.Start = …`
`Task.PercentComplete = 100` | `ActualStart` / `ActualFinish` プロパティへ書き込み、エンジンに再計算させる。 |
| 作業規模(期間)の変更を反映したい | 開始日・終了日を直接上書き | `Task.Duration` を更新し、カレンダーと依存関係に従って終了日を自動計算させる。 |
| 手動入力タスクとして扱いたい | 自動タスクのまま日付を乱打 | 明示的に `Task.Manual = True` に倒してから日付を設定する。 |

特に重要なのが「実績」の反映です。現実にタスクが始まった/終わったのであれば、触るべきは`Start/Finish`ではなく`ActualStart/ActualFinish`です。実績プロパティを更新すれば、制約条件を汚染することなく、エンジンが残余タスクを適切に未来へリスケジュールしてくれます。

—

3. プロダクション仕様のセーフティ同期モジュール

それでは、外部連携などでタスクの日程情報を安全かつ高速に同期するためのプロダクションコードを提示します。

このクラスモジュール(または標準モジュール)では、以下のエンタープライズ要件を満たす設計を施しています。

1. スクリーンの描画停止と計算エンジンの手動化(超高速化)
2. トランザクション的エラーハンドリング(障害発生時の確実なエンジン復帰)
3. 副作用のない制約制御(意図しない制約付与の完全遮断)
4. 実績プロパティを用いたスケジューリング整合性の維持

Option Explicit

‘ ==============================================================================
‘ モジュール名: Mod_SafeScheduleIntegrator
‘ 説明: MS Project計算エンジンの制約条件を破壊せずに外部データを安全に同期する
‘ 責務: 高速一括処理、制約整合性の担保、計算ライフサイクルの厳格な管理
‘ ==============================================================================

Public Enum ScheduleSyncMode
SyncAsActuals = 1 ‘ 実績として反映(ActualStart / ActualFinish)
SyncAsStrictConstraint = 2 ‘ 厳格な制約として反映(Must Start On 等)
SyncAsWorkDuration = 3 ‘ 期間として反映(先行関係を優先しDurationのみ調整)
End Enum

”’

”’ 安全にタスク日程を外部データと同期するエントリーポイント
”’

Public Sub SafeUpdateTaskSchedule( _
ByVal targetTask As MSProject.Task, _
ByVal newStartDate As Date, _
ByVal newFinishDate As Date, _
ByVal syncMode As ScheduleSyncMode)

If targetTask Is Nothing Then Exit Sub

‘ 手動スケジュールタスク(Manually Scheduled)の場合は日付直書きが許容されるが、
‘ 自動スケジュール(Auto Scheduled)の場合はエンジン保護ロジックを強制する
If targetTask.Manual Then
‘ 手動タスクはエンジンがバイパスされているため直接代入可能
targetTask.Start = newStartDate
targetTask.Finish = newFinishDate
Exit Sub
End If

Select Case syncMode
Case ScheduleSyncMode.SyncAsActuals
ApplyActualDates targetTask, newStartDate, newFinishDate

Case ScheduleSyncMode.SyncAsStrictConstraint
ApplyStrictConstraint targetTask, newStartDate, newFinishDate

Case ScheduleSyncMode.SyncAsWorkDuration
ApplyDurationOnly targetTask, newStartDate, newFinishDate

Case Else
Err.Raise vbObjectError + 51001, “SafeUpdateTaskSchedule”, “未定義の同期モードです。”
End Select
End Sub

”’

”’ 実績日付として反映(制約条件を一切汚染しないアプローチ)
”’

Private Sub ApplyActualDates(ByVal t As MSProject.Task, ByVal aStart As Date, ByVal aFinish As Date)
‘ 実績開始日を設定
t.ActualStart = aStart

‘ 完了日が過去、または明確に終了している場合のみ実績終了日を設定
If aFinish > 0 And aFinish <= Now Then t.ActualFinish = aFinish End If End Sub '''

”’ 制約条件を明示的に指定して同期(エンジンの副作用を制御下におくアプローチ)
”’

Private Sub ApplyStrictConstraint(ByVal t As MSProject.Task, ByVal cStart As Date, ByVal cFinish As Date)
‘ 日付を直書きするのではなく、制約タイプと制約日付をペアで「明示的」に書き込む
‘ これにより、意図しない SNET や FNET の暗黙生成を防止する

‘ 例:指定日開始厳守(Must Start On)
t.ConstraintType = pjMSO
t.ConstraintDate = cStart

‘ 終了日は直接書かず、所要期間(Duration)を計算して割り振る(カレンダーを考慮)
‘ 注意: ProjectのDurationは「分」単位で管理されるため、Application.DateDifference を使用
Dim durationMinutes As Long
durationMinutes = Application.DateDifference(cStart, cFinish, t.Calendar)

If durationMinutes >= 0 Then
t.Duration = durationMinutes
Else
‘ 終了日が開始日より前にある異常データへの耐性
Err.Raise vbObjectError + 51002, “ApplyStrictConstraint”, _
“終了日が開始日よりも前に設定されています: Task ID=” & t.ID
End If
End Sub

”’

”’ 先行リンク関係を崩さず、所要時間のみをアジャストするアプローチ
”’

Private Sub ApplyDurationOnly(ByVal t As MSProject.Task, ByVal cStart As Date, ByVal cFinish As Date)
‘ 制約が ASAP (できるだけ早く) になっていることを保証
t.ConstraintType = pjASAP
t.ConstraintDate = “NA”

‘ 期間のみを変更し、開始日は先行タスクの完了通知(CPM計算)に委ねる
Dim durationMinutes As Long
durationMinutes = Application.DateDifference(cStart, cFinish, t.Calendar)

If durationMinutes > 0 Then
t.Duration = durationMinutes
End If
End Sub

このモジュールを回すバッチ処理の完全版オーケストレーター

単一タスクの書き換えロジックが美しくても、何千行ものタスクを同期するループ処理で毎回エンジンが再計算を実行していては、処理時間が指数関数的に増大し、最悪クラッシュします。

以下のバッチ制御コードを実装してください。

”’

”’ 外部データソースから数千件のタスクを一括同期する本番用オーケストレーター
”’

Public Sub BatchSynchronizeTasks()
Dim app As MSProject.Application
Set app = MSProject.Application

‘ — 1. エンジン停止と画面描画抑制(超高速化の必須儀式) —
Dim originalCalcMode As PjCalculation
originalCalcMode = app.Calculation

On Error GoTo FATAL_HANDLER

app.ScreenUpdating = False
app.Calculation = pjManual ‘ 自動計算を遮断

‘ — 2. データ同期処理ループ(外部DB/Excel等からの読み込みを想定) —
Dim prj As MSProject.Project
Set prj = app.ActiveProject

Dim t As MSProject.Task
For Each t In prj.Tasks
If Not t Is Nothing Then
If Not t.Summary And Not t.ExternalTask Then
‘ 例: モックデータによる同期実行(実際はDBから読み込んだ日付)
Dim mockStart As Date: mockStart = DateSerial(2024, 6, 1)
Dim mockFinish As Date: mockFinish = DateSerial(2024, 6, 15)

‘ 実績優先モードで更新
SafeUpdateTaskSchedule t, mockStart, mockFinish, ScheduleSyncMode.SyncAsActuals
End If
End If
Next t

‘ — 3. プロジェクト全体の再計算(CPMエンジンの安全な再起動) —
app.Calculation = originalCalcMode
app.CalculateProject
app.ScreenUpdating = True

MsgBox “同期処理が安全に完了しました。”, vbInformation, “処理完了”
Exit Sub

FATAL_HANDLER:
‘ 途中でクラッシュしても、計算設定と画面描画だけは絶対に復元する
app.Calculation = originalCalcMode
app.ScreenUpdating = True
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical, “システムエラー”
End Sub

—

4. アーキテクトが教える「運用の鉄則」

最後に、ツールを組織に展開する上で避けて通れない3つの鉄則を共有します。

① 「手動タスク」と「自動タスク」を混在させない

MS Project 2010以降に導入された「手動スケジュール(Manually Scheduled)」は、計算エンジンを完全にオフにするExcelライクな機能です。エンドユーザーが気まぐれに手動タスクを混ぜると、依存関係の連動が死にます。
VBA側で定期的に以下のチェックを走らせ、エンタープライズ計画では自動タスクへの統一を強制することを推奨します。

If t.Manual Then t.Manual = False ‘ 強制的に自動スケジュール化

② 制約条件の「ゴミ」を検知してパージする

誰かが手作業でStart日付を直接編集してしまったプロジェクトは、タスク一覧に`pjSNET`や`pjFNET`の制約がゴミのように散乱しています。
これらを一掃し、本来のクリティカルパスを取り戻すためのクリーナー処理をメンテツールに仕込んでおきましょう。

‘ 制約条件のパージ処理(ASAPへ回帰させる)
If t.ConstraintType <> pjASAP And t.ActualStart = 0 Then
‘ 実績が入っていない未来タスクの不要な制約をリセット
t.ConstraintType = pjASAP
t.ConstraintDate = “NA”
End If

③ 日付ではなく「期間」と「リレーション」で語らせる

外部システムからMS Projectにデータを取り込む設計をする際、安易に「開始日」「終了日」の両方を要求仕様に含めないでください。
Projectに投入すべき本質的なデータは、以下の3点です。

1. 先行後続リンク(Predecessors / Successors)
2. タスクの期間(Duration)
3. プロジェクト全体の基準開始日(Project Summary Start Date)

この3つさえあれば、カレンダー(休日や稼働時間)を考慮した正確な日付はMS Project自身が美しく弾き出します。日付を直接同期しようとするから、破綻するのです。

—

5. まとめ

  • `Task.Start` / `Task.Finish` への直接代入は、暗黙的な制約(SNET/FNET)を生み、CPMエンジンを殺す行為である。
  • 実績の同期には `ActualStart` / `ActualFinish` を使い、制約条件の汚染を防ぐ。
  • 外部連携バッチを組む際は、`Application.Calculation = pjManual` でエンジンを止め、全件処理後に `CalculateProject` で一括解決するのがパフォーマンスと安全性の絶対解である。

MS Projectの真価は、不確実な変更が発生したときに「スケジュールがどう歪み、どこが新たなクリティカルパスになるかをシミュレーションできること」にあります。

エンジンの計算ロジックをリスペクトし、制約条件を正しく制御できる堅牢なツールを設計してください。

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