Taskの「Duration」単位揺らぎを制す:MS Project VBAにおける極限の工数正規化アーキテクチャ
Microsoft Projectを基幹としたリソース・工数管理システムにおいて、現場からのインポートデータや外部システム(ExcelやWebフォーム等)からの連携データに潜む最大の罠、それが「Duration(期間)の単位の揺らぎ」だ。
「3d」「24h」「1440m」――人間にとっては同義であっても、MS Projectのオブジェクトモデルにとって、これらは型や解釈の異なる地雷原である。特にProjectの内部エンジンは、プロジェクト全体のカレンダー設定(1日の稼働時間、週の稼働日など)に依存してdurationの長整数値(Tenths of minutes: 分の10分の1)を動的に解釈するため、単純な文字列置換や力技のパースでは必ず破綻する。
本稿では、レガシーなVBA環境において、オブジェクトのライフサイクルを完全に制御し、メモリリークを排除しながら、いかなる単位の揺らぎをも完璧に吸収してProject標準の分単位へと正規化する極限のロジックを提示する。
—
1. MS Projectオブジェクトモデルの罠と単位の真実
MS Projectの `Task.Duration` プロパティは、VARIANT型(内部的にはLong)として保持され、デフォルトでは「分の10分の1(Tenths of minutes)」を格納する。例えば、1日が8時間設定のプロジェクトにおいて「1日 (1d)」は `480`(分)× `10` = `4800` という整数値になる。
しかし、外部から読み込んだ文字列(例: `”1.5日”`, `”12時間”`, `”720分”`)をそのまま代入しようとすると、Projectのロケール設定やパースエンジンの気まぐれにより、意図しない解釈がなされるか、最悪の場合、実行時エラー(Type Mismatch)を引き起こす。
この揺らぎを安全に吸収するためには、以下のアプローチが不可欠である。
1. 入力文字列から数値と単位(m, h, d 等)を厳密に分離する。
2. プロジェクトのカレンダー設定(1日の分数)を動的に取得し、基準となる「分」へ換算する。
3. Projectオブジェクトの参照を適切に管理し、メモリ上のゾンビプロセス化を防ぐ。
—
2. 実装コード:堅牢な工数正規化エンジン
以下に、実務の現場で即座に稼働させることができる、エンタープライズグレードのVBAモジュールを示す。正規表現(VBScript.RegExp)を用いて単位を完全分解し、Projectのネイティブ仕様に準拠した変換を行う。
Option Explicit
‘ ==============================================================================
‘ 担当アーキテクト注記:
‘ TaskのDuration正規化処理
‘ 外部から持ち込まれる多様な単位(分/時間/日/週)を安全にパースし、
‘ Project標準の分単位へ変換してTaskオブジェクトへ流し込む。
‘ ==============================================================================
Public Sub NormalizeTaskDurations()
Dim prj As MSProject.Project
Set prj = ActiveProject
‘ パフォーマンス最適化のため画面描画と自動再計算を停止
Application.ScreenUpdating = False
Application.Calculation = pjManual
On Error GoTo ErrorHandler
Dim tsk As MSProject.Task
Dim rawValue As String
Dim normalizedMinutes As Long
For Each tsk in prj.Tasks
‘ 予約タスクやサマリータスク、空行の排除
If Not tsk Is Nothing Then
If Not tsk.Summary And tsk.ExternalTask = False Then
‘ ここでは仮にタスクのテキストフィールド(Text1)に入力された
‘ 揺らぎのある工数文字列を読み込む想定とする
rawValue = Trim(CStr(tsk.Text1))
If rawValue <> “” Then
‘ 単位を解析して「分」に変換するコア関数を呼び出し
normalizedMinutes = ParseDurationToMinutes(rawValue, prj)
If normalizedMinutes >= 0 Then
‘ MS ProjectのDurationは「分の10分の1」で保持されるため 10 を乗算
tsk.Duration = normalizedMinutes 10
Else
‘ パースエラー時のフォールバックまたはログ記録
Debug.Print “Parsing Error on Task ID: ” & tsk.ID & ” Value: ” & rawValue
End If
End If
End If
End If
‘ ループ内でのオブジェクト参照解放の配慮(VBAの仕様上、変数のスコープ管理に依存)
Next tsk
CleanUp:
‘ 状態の復元
Application.Calculation = pjAutomatic
Application.ScreenUpdating = True
Set prj = Nothing
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “System Error”
Resume CleanUp
End Sub
‘ ==============================================================================
‘ 単位揺らぎ吸収コア関数
‘ ==============================================================================
Private Function ParseDurationToMinutes(ByVal inputStr As String, ByRef prj As MSProject.Project) As Long
Dim regEx As Object
Set regEx = CreateObject(“VBScript.RegExp”)
‘ 数値(小数含む)と単位部分をキャプチャする正規表現
‘ 例: “1.5d”, “12h”, “720m”, “3日”, “5時間”
regEx.Pattern = “^\s([0-9]+(?:\.[0-9]+)?)\s([a-zA-Z旨分時間日週間wmdhms])\s$”
regEx.IgnoreCase = True
regEx.Global = False
If Not regEx.Test(inputStr) Then
ParseDurationToMinutes = -1 ‘ パース失敗
Exit Function
End If
Dim matches As Object
Set matches = regEx.Execute(inputStr)
Dim numericValue As Double
Dim unitStr As String
numericValue = CDbl(matches(0).SubMatches(0))
unitStr = LTrim(RTrim(LCase(matches(0).SubMatches(1))))
‘ プロジェクトの1日あたりの分数・1週間あたりの日数を動的取得
‘ (硬直したハードコーディングを排除し、プロジェクト設定に追従させる)
Dim minutesPerDay As Double
Dim minutesPerHour As Double
minutesPerHour = 60#
‘ Projectのカレンダー設定から「1日の稼働時間(分)」を取得
minutesPerDay = CDbl(prj.MinutesPerDay)
Select Case unitStr
Case “m”, “分”, “min”, “mins”
ParseDurationToMinutes = CLng(numericValue)
Case “h”, “時間”, “hour”, “hours”
ParseDurationToMinutes = CLng(numericValue minutesPerHour)
Case “d”, “日”, “day”, “days”
ParseDurationToMinutes = CLng(numericValue minutesPerDay)
Case “w”, “週”, “week”, “weeks”
‘ 1週間あたりの稼働日数を加味(MinutesPerWeek / MinutesPerDay が確実)
Dim minutesPerWeek As Double
minutesPerWeek = CDbl(prj.MinutesPerWeek)
ParseDurationToMinutes = CLng(numericValue minutesPerWeek)
Case Else
‘ 単位が省略された場合はデフォルトで「時間」または「日」とみなす、あるいはエラー
‘ ここでは安全のためプロジェクトのデフォルト単位(通常は日)を適用
ParseDurationToMinutes = CLng(numericValue minutesPerDay)
End Select
‘ オブジェクトの明示的解放(メモリ最適化)
Set matches = Nothing
Set regEx = Nothing
End Function
—
3. チーフアーキテクトが説く「メモリ管理とパフォーマンスの極意」
大規模なProjectファイル(数千〜数万タスク)を扱う際、VBAのメモリ管理とオブジェクトライフサイクルを誤ると、COMコンポーネントのリークや、最悪の場合はProjectプロセスのクラッシュを招く。
1. 描画と計算エンジンの完全停止
`Application.ScreenUpdating = False` と `Application.Calculation = pjManual` のペアは必須である。タスクのDurationを変更するたびにProjectがクリティカルパスの再計算やガントチャートの再描画を行うと、処理時間が幾何級数的に跳ね上がる。これを一括して手動モードに切り替えることで、処理速度を数十倍に高めることができる。
2. COMオブジェクトの遅延バインディングと即時破棄
上記のコードでは `VBScript.RegExp` を都度生成しているが、ループの外でインスタンス化して使い回すか、あるいは適切なタイミングで `Set … = Nothing` を行うことが、長大なループ処理におけるメモリ肥大化(メモリリーク)を防ぐ防壁となる。VBAのガベージコレクションは頼りにならない。開発者がスコープを抜ける前に明示的に参照を切断する意志を持たなければならない。
3. プロジェクト設定への完全準拠
ハードコーディングで「1日は480分」と決めつけるのは、マルチテナントや多様な顧客カレンダーを扱うシニアエンジニアの恥だ。`prj.MinutesPerDay` や `prj.MinutesPerWeek`といったProjectネイティブのプロパティから動的にコンテキストを汲み取り、計算の基準とする。この設計思想こそが、環境変化に耐えうるレジリエントなシステムを生み出す。
—
総括
VBAはレガシーな言語と揶揄されることもあるが、その下層にあるCOMインターフェースとAPIの挙動を完全に掌握していれば、企業の基幹を支える強靭な自動化エンジンへと昇華させることができる。
「たかが工数の単位変換」と侮るなかれ。データインテグリティの崩壊は、プロジェクト管理の死活問題に直結する。本稿で示した正規化ロジックとオブジェクトライフサイクルの制御手法を武器に、あなたの管理するProject環境を極限まで最適化してほしい。
