【Project VBA極限知見】MS Projectのスケジュールエンジンに頼るな!VBAによる堅牢な「終了日」自動算出ロジックの全貌
開発現場でMicrosoft Project(以下、Project)を使ったWBSの自動生成や、外部からのタスク一括登録ツールを作ったことがある者なら一度は直面するはずだ。
「VBAからタスクの期間と開始日を入れたのに、終了日が想定通りに計算されない。あるいはスケジュールエンジンの再計算が走って処理が重すぎる」と。
素朴な初心者は、Projectのオブジェクトモデルに対して `Task.Duration` と `Task.Start` だけを放り込み、あとは野となれ山となれと放置する。しかし、企業の現場で求められるのは「いつ始まり、いつ終わるのか」の厳密なデッドラインの把握だ。Projectの自動スケジュールエンジンは、カレンダー設定やリソース制約によって予期せぬ日付のズレ(シフト)を起こす。
今回は、Project VBAの挙動の裏側を暴き、「余計なエンジンの気まぐれに頼らず、VBA側でカレンダー(稼働日・休日)を完全に制御して終了日を算出する」ための、プロダクション品質の堅牢なコードと設計思想を伝授しよう。
—
1. なぜProject標準のスケジュールに丸投げしてはいけないのか?
Projectはタスクの「開始日(Start)」と「期間(Duration)」から「終了日(Finish)」を自動計算する機能を持っている。しかし、これをそのままVBAで大量処理(数千行のWBS構築など)に使うと、以下の致命的な問題が発生する。
1. 計算のオーバーヘッド:1行書き換えるたびにProjectのスケジュールエンジンが全体を再計算し、処理が極端に遅くなる。
2. 暗黙の制約(Constraints):タスクにデフォルトで「できるだけ早く(As Soon As Possible)」などの制約がつき、意図しない日付にスナップされる。
3. カレンダーの差異:プロジェクト標準カレンダー、リソース別カレンダー、例外日(祝日や年末年始)の考慮漏れによる致命的なズレ。
実務で使えるツールを作るなら、「計算すべき日付はVBA側でロジカルに弾き出し、結果としての完成形をProjectに書き込む」というアプローチをとるべきだ。これにより、圧倒的なパフォーマンスと、誰が見ても矛盾のないスケジュールが担保される。
—
2. 堅牢な日付計算ロジックの設計思想
今回実装するのは、「指定された開始日から、稼働日ベースで『期間(日数)』を加算し、土日・祝日を除外して正確な終了日を算出する」ロジックだ。
実務においては、以下の要素をクリアする必要がある。
- 単位の換算:Projectの「期間」はデフォルトで「分(Minutes)」や「日(Days)」で保持される。これを正確に日数に落とし込む。
- 休日判定:土日だけでなく、企業の独自休日(祝日や会社創立記念日など)を配列またはワークシートから高速にルックアップできるようにする。
- 時刻の正規化:開始日の時刻(例: 08:00)と終了日の時刻(例: 17:00)の整合性を保つ。
—
3. 【コピペOK】プロダクションコード例
以下のコードは、Project VBAの標準モジュールにそのまま貼り付けて実行できる、実務仕様の関数群だ。
単に日付を足すだけでなく、例外日(祝日)を考慮したカレンダーロジックを内蔵している。
Option Explicit
‘ =========================================================================
‘ ódulo名: 稼働日を考慮したスケジュール計算モジュール
‘ 概要: MS Projectのエンジンに依存せず、VBA単体で正確な終了日を算出する
‘ =========================================================================
Public Sub ApplyCalculatedSchedules()
Dim t As Task
Dim lngDurationDays As Long
Dim dtmStart As Date
dim dtmFinish As Date
‘ 画面描画と自動計算を停止して爆速化
Application.ScreenUpdating = False
Application.Calculation = pjManual
On Error GoTo ErrorHandler
For Each t ActiveProject.Tasks
‘ 概要タスク(サマリー)やマイルストーンは除外
If Not t Is Nothing Then
If Not t.Summary And t.Duration > 0 Then
dtmStart = t.Start
‘ ProjectのDurationは分単位で保持されているため、「日」に変換(1日 = 480分 = 8時間稼働を想定)
‘ ※環境の「1日の稼働時間設定」に合わせて係数を変更してください
lngDurationDays = CLng(t.Duration / 480)
If lngDurationDays < 1 Then lngDurationDays = 1
' 自作の稼働日加算ロジックで終了日を算出
dtmFinish = CalculateWorkingFinishDate(dtmStart, lngDurationDays)
' 計算結果を書き込む(ここで初めてProjectに値を渡す)
t.Finish = dtmFinish
End If
End If
Next t
MsgBox "スケジュールの算出が完了しました。", vbInformation, "完了"
CleanUp:
Application.ScreenUpdating = True
Application.Calculation = pjAutomatic
Exit Sub
ErrorHandler:
MsgBox "エラーが発生しました: " & Err.Description, vbCritical, "異常終了"
Resume CleanUp
End Sub
' =========================================================================
' 効用: 開始日と稼働日数から、土日祝日を除外した正確な終了日を返す
' =========================================================================
Private Function CalculateWorkingFinishDate(ByVal startDate As Date, ByVal workingDays As Long) As Date
Dim currentDate As Date
Dim addedDays As Long
currentDate = startDate
addedDays = 0
' 期間が1日の場合は開始日=終了日とする(必要に応じて調整)
If workingDays > 1 Then
Do While addedDays < (workingDays - 1)
currentDate = DateAdd("d", 1, currentDate)
' 稼働日(土日でも祝日でもない)の場合のみカウントを進める
If IsWorkingDay(currentDate) Then
addedDays = addedDays + 1
End If
Loop
End If
' 終了日の時刻を開始日と同じ時刻(または定時)に合わせる
CalculateWorkingFinishDate = DateSerial(Year(currentDate), Month(currentDate), Day(currentDate)) + TimeValue(startDate)
End Function
' =========================================================================
' 効用: 指定された日付が稼働日(土日・祝日以外)か判定する
' =========================================================================
Private Function IsWorkingDay(ByVal targetDate As Date) As Boolean
Dim wDay As VbDayOfWeek
wDay = Weekday(targetDate, vbMonday)
' 土曜日(6)、日曜日(7)は非稼働日
If wDay = 6 Or wDay = 7 Then
IsWorkingDay = False
Exit Function
End If
' 祝日判定(ハードコーディングの例。実務ではExcelやDBから配列として読み込むべき)
If IsHolidays(targetDate) Then
IsWorkingDay = False
Exit Function
End If
IsWorkingDay = True
End Function
' =========================================================================
' 効用: 祝日マスタ判定(実務では外部データソース連携を推奨)
' =========================================================================
Private Function IsHolidays(ByVal targetDate As Date) As Boolean
Dim holidays As Variant
Dim i As Long
' 【サンプル】固定の祝日リスト(実際にはマスタシートやDBから動的に取得すること)
holidays = Array("2026-01-01", "2026-01-12", "2026-02-11", "2026-02-23", "2026-04-29", _
"2026-05-03", "2026-05-04", "2026-05-05", "2026-07-20", "2026-08-11", _
"2026-09-21", "2026-09-22", "2026-10-12", "2026-11-03", "2026-11-23")
For i = LBound(holidays) To UBound(holidays)
If targetDate = CDate(holidays(i)) Then
IsHolidays = True
Exit Function
End If
Next i
IsHolidays = False
End Function
---
4. プロダクション環境における注意点:データベース・ファイル連携
上記のコードは単体で美しく動作するが、実務の現場(Enterprise環境)ではさらに一歩進んだ設計が求められる。
1. 祝日マスタのハードコーディングを避ける
コード内に祝日を直書きするのは、翌年の法改正やイレギュラーな休日変更に対応できない「負の遺産」を生む。必ずExcelの別シート、あるいは外部データベース(SQL Serverなど)から `ADODB.Recordset` 等を介して配列としてメモリ上に読み込み、`IsHolidays` 関数に渡すアーキテクチャにすること。
2. パフォーマンスチューニングの鉄則
コード内にも入れているが、`Application.Calculation = pjManual` による手動計算モードへの切り替えは必須だ。これを行わないと、数千行のタスクに対して `t.Finish = …` を実行するたびにProjectのGUIが再描画され、ツールがフリーズしたかのような遅延を招く。
3. タイムゾーンと時刻の罠
VBAの `Date` 型は時刻を含んでいる。Project上で「8時間=1日」とする場合、開始時刻が不規則だと(例:前日の17:00開始など)、日付の繰り上がりが狂う。必ず `TimeValue` や時間計算の基準をプロジェクト全体の稼働時間に一致させよ。
—
チーフアーキテクトからの総括
初心者向けのチュートリアルでは「オブジェクトに値を代入すれば勝手に動く」と教えられる。しかし、プロの現場で求められるのは、「システムの機嫌を損ねず、予測可能な結果を高速に叩き出す」ことだ。
今回紹介した「VBA側でカレンダーを握り、計算し尽くした結果をProjectに書き込む」という設計アプローチは、WBS自動生成ツールだけでなく、リソース配分や進捗予測シミュレーターなど、あらゆる高度なプロジェクト管理自動化の基盤となる。
安易な標準機能への依存を断ち切り、コードの主導権を常にあなたのVBAに持たせること。それこそが、トラブルゼロの堅牢な業務自動化システムを構築する唯一の道である。
