【実務・中級編】Taskの「Duration」単位(分・時間・日)の揺らぎをVBAで吸収する正規化処理 – Project VBA解析バイブル

スポンサーリンク

【Project VBAを掌握する極限の知見】Taskの「Duration」単位の揺らぎを完全に制圧するデータクレンジング設計

現場のPMから上がってくる進捗データ、あるいは外部システムから吐き出されたCSV。これらをMS Projectにインポート、あるいはVBAで集計する際、最もエンジニアを絶望させる要因は何だと思うか?

それは、「Duration(期間)の単位の揺らぎ」だ。

ある担当者は `8h` と書き、ある者は `1d` と書き、別のシステムは `480m`(分)というミリ秒単位のような狂った数値を渡してくる。MS Projectのオブジェクトモデルにおいて、`Task.Duration` は内部的には「分(Minutes)」のLong型として保持されているが、これを安易に `Task.Duration = val` などと代入すると、プロジェクトのカレンダー設定(1日何時間労働か)や、表示単位のマスクに足をすくわれ、データが完全に破損する。

今回は、このカオスな単位の揺らぎを完全に無力化し、いかなる入力値であっても正確にプロジェクト標準の「分」へと正規化する、実務直結のプロダクションコードを授けよう。

1. なぜ「単位の揺らぎ」でVBAは破綻するのか?

多くの初学者は、`Task.Duration` に数値をそのまま代入すればいいと考える。
しかし、Project VBAの仕様を熟知していれば、それが悪夢の始まりだと知っている。

落とし穴1: 表示単位(Minutes / Hours / Days)の罠

MS Projectでは、プロジェクトオプションで「1日=8時間」「1週=40時間」といった定義がなされている。しかし、入力された文字列(例: “2d”)をそのままパースせず、単なる数値として扱ったり、プロジェクトの稼働日カレンダーを無視した計算を組み込むと、土日や祝日を跨いだ瞬間に数時間のズレが生じる。

落とし穴2: 型の不一致と暗黙の型変換

VBAの `Variant` や文字列をそのまま `Task.Duration` に渡すと、Projectのエンジンは独自の解釈でそれをパースする。これが多国籍チームや混在データ環境において、予期せぬオーバーフローや型ミスマッチエラーを引き起こす。

【結論】
入力されたデータが「分(m)」「時間(h」「日(d)」のいずれであっても、VBA側で厳密に正規化(パース)し、最終的にProjectの最小管理単位である「分(Minutes)」のLong値へ確実に変換してからオブジェクトへ流し込む。これが唯一にして絶対の正解である。

2. 堅牢な正規化エンジンのアーキテクチャ

今回提供するコードは、単なる文字列置換ではない。以下の要件を満たす「実務仕様」だ。

1. 柔軟なパース: 「2.5d」「4h」「240m」「12」といった様々な表記ゆれ(前後の空白や単位の大文字小文字)を正規表現または厳密な文字列操作でハンドリングする。
2. デフォルト単位のフォールバック: 単位が省略された数値(例: `5`)が渡された場合の挙動を安全に制御する。
3. トランザクション的処理: エラーが発生したタスクで処理を止めず、ログを残しながらバッチ処理を完遂させる。

3. 【コピペ即実戦投入可】Duration正規化&代入プロシージャ

以下のコードは、Excelや外部テキストから取得した異形のDuration文字列を解析し、ActiveProjectのタスクへ安全に流し込むための完全版モジュールだ。

Option Explicit

‘ ==============================================================================
‘ 処理名: NormalizeAndApplyDuration
‘ 概要: 単位の揺らぎ(m, h, d)を含む文字列を解析し、プロジェクト標準の「分」に
‘ 変換した上で、指定タスクのDurationに安全に設定する。
‘ ==============================================================================
Public Sub NormalizeAndApplyDuration(ByVal targetTask As Task, ByVal rawValue As Variant, Optional ByVal defaultUnit As String = “h”)

On Error GoTo ErrorHandler

Dim normalizedMinutes As Long
normalizedMinutes = ParseDurationToMinutes(rawValue, defaultUnit)

‘ Projectオブジェクトモデルへの書き込み
‘ Task.Durationの内部単位は「分」であるため、そのまま代入する
targetTask.Duration = normalizedMinutes

Exit Sub

ErrorHandler:
‘ 実務ではここでエラーログ出力用ファンクションを呼び出すこと
Debug.Print “Error setting duration for Task ID ” & targetTask.ID & “: ” & Err.Description
End Sub

‘ ==============================================================================
‘ 関数名: ParseDurationToMinutes
‘ 概要: あらゆる表記ゆれのDuration文字列を「分(Minutes)」のLong値に変換する
‘ ==============================================================================
Public Function ParseDurationToMinutes(ByVal rawValue As Variant, Optional ByVal defaultUnit As String = “h”) As Long

If IsNull(rawValue) Or IsEmpty(rawValue) Then
ParseDurationToMinutes = 0
Exit Function
End If

Dim strVal As String
strVal = Trim(CStr(rawValue))

If strVal = “” Then
ParseDurationToMinutes = 0
Exit Function
End If

‘ 小文字に統一し、不要な空白を除去
strVal = LCase(Replace(strVal, ” “, “”))

‘ 単位の抽出と数値の分離
Dim numericPart As Double
Dim unitPart As String

‘ 末尾の文字がアルファベット(単位)か判定
Dim lastChar As String
lastChar = Right(strVal, 1)

Select Case lastChar
Case “m”, “h”, “日”, “d”
‘ 日本語の「日」や英語の “d”, “h”, “m” をハンドリング
If lastChar = “日” Then
unitPart = “d”
Else
unitPart = lastChar
End If
numericPart = Val(Left(strVal, Len(strVal) – 1))

Case Else
‘ 単位が省略されている場合はデフォルト単位を採用
If IsNumeric(strVal) Then
numericPart = Val(strVal)
unitPart = LCase(defaultUnit)
Else
‘ パース不可能な文字列の場合は 0 を返す(または例外スロー)
ParseDurationToMinutes = 0
Exit Function
End If
End Select

‘ プロジェクトのカレンダー設定に基づく換算係数
‘ MS Projectのデフォルト: 1日 = 480分 (8時間), 1時間 = 60分
‘ ※厳密には Application.MinutesPerDay 等を参照すべきだが、汎用的な定数として定義
Dim calculatedMinutes As Long

Select Case unitPart
Case “m” ‘ 分
calculatedMinutes = CLng(numericPart)

Case “h” ‘ 時間 (1h = 60m)
calculatedMinutes = CLng(numericPart 60)

Case “d” ‘ 日 (1d = 480m ※8時間労働を仮定)
‘ ※もしプロジェクトのカレンダー設定に厳密に従わせたい場合は
‘ ActiveProject.MinutesPerDay を乗算すること
Dim minutesPerDay As Long
minutesPerDay = ActiveProject.MinutesPerDay
If minutesPerDay <= 0 Then minutesPerDay = 480 ' フォールバック calculatedMinutes = CLng(numericPart minutesPerDay) Case Else calculatedMinutes = CLng(numericPart) ' フォールバック End Select ParseDurationToMinutes = calculatedMinutes End Function ---

4. コードの深層解説:なぜこの実装が「プロフェッショナル」なのか?

1. `ActiveProject.MinutesPerDay` への動的追従

上記のコード内で、日(d)を分に変換する際、ハードコーディングで `480` を掛けないように設計している点に注目してほしい。
プロジェクトによっては「1日7時間労働(420分)」や「変形労働時間制」を採用しているケースがある。`ActiveProject.MinutesPerDay` を動的に参照することで、プロジェクトごとのカレンダー差異による致命的な工数ズレを完全に回避している。

2. フォールバックと堅牢性の担保

外部からのインポートデータは、往々にしてゴミデータが混ざる。「`5.5`(単位なし)」や「`2h`」「`1.5日`」といったマルチな入力を、`Val` 関数と文字列末尾の判定によって綺麗に切り分けているため、データクレンジングの工数を極限まで削減できる。

3. オファリングの分離(SRPの原則)

「文字列を解析して分に変換するロジック(`ParseDurationToMinutes`)」と、「タスクオブジェクトに適用するロジック(`NormalizeAndApplyDuration`)」を完全に分離している。これにより、単体テスト(Unit Test)が容易になり、将来的な仕様変更(例:Excel出力時やDB登録時の逆変換など)にも耐えうる設計となっている。

5. 実務運用のためのアーキテクトからの提言

大規模なWBSや数千行に及ぶタスク群に対してこの処理を流し込む場合、画面の描画更新(ScreenUpdating)やイベントハンドラがボトルネックになり、パフォーマンスが著しく低下する。

バッチ処理を行う際は、必ず以下の鉄則をコードの前後に入れること。

‘ 処理高速化の常套手段
Application.ScreenUpdating = False
‘ …ここにバッチ処理ループ…
Application.ScreenUpdating = True

Project VBAを制する者は、スケジュール管理の自動化を制する。
単なる「動くコード」ではなく、「破綻しない構造」を常に意識し、あなたの現場の生産性を極限まで引き上げてほしい。

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