【実務・中級編】【実務中級】外部CSVからの制約条件インポート:マッピングテーブルを用いた柔軟なデータ連携エンジンの構築 – Project VBA解析バイブル

スポンサーリンク
スポンサーリンク

Project VBAを掌握する極限の知見:マッピングテーブルを用いた制約条件インポート・エンジンの構築

プロジェクト管理の現場において、数百、数千のタスクが並ぶスケジュール管理は、もはや「手入力」の領域を脱している。特に、外部の計画策定ツールやExcelで管理された「制約条件(Constraint)」をMicrosoft Projectへ同期させる作業は、ミスが許されない極めてクリティカルなプロセスだ。

しかし、多くの開発者が陥る罠がある。それは、「Projectのタスクをループし、その中で毎回外部ファイルを探しに行く」という、計算量が爆発する、極めて非効率で脆弱なコードを書いてしまうことだ。

本記事では、単なる「CSV読み込みスクリプト」ではない。スケーラビリティ、堅牢性、そしてパフォーマンスを兼ね備えた、「マッピングテーブルを用いた制約条件インポート・エンジン」の設計思想とその実装を伝授する。

1. なぜ、あなたのスクリプトは「現場」で使い物にならないのか

多くの「中級者」が書くコードは、以下のような論理構造になっている。

1. Projectの全タスクを `For Each` で回す。
2. 各タスクに対し、CSVファイルを一行ずつ読み込み、一致するIDがないか探す。
3. 一致したら制約条件を適用する。

この設計は、計算量(Time Complexity)の観点から見て致命的だ。タスク数が $N$、CSVの行数が $M$ とすると、計算量は $O(N \times M)$ となる。タスクが1,000、CSVが1,000行あれば、100万回の比較演算が発生する。これは「動く」かもしれないが、「プロフェッショナルなツール」ではない。

さらに、外部データの「Task Name」をキーにしている場合、名前の重複や微細な表記揺れによって、全く別のタスクに制約が適用されるという、プロジェクトを崩壊させかねないリスクを孕んでいる。

プロフェッショナルが取るべき設計指針

  • データ構造の分離: 「外部データ」と「Projectオブジェクト」を直接結びつけず、中間層(マッピングテーブル)を介する。
  • ハッシュマップの活用: CSVデータを事前に `Scripting.Dictionary` に格納し、検索コストを $O(1)$ に抑える。
  • 一意性の担保: Task Nameではなく、WBSコードやカスタムプロパティなどの「一意な識別子(Unique ID)」をキーとして設計する。

2. エンジンのアーキテクチャ設計

我々が構築するのは、以下の3層構造を持つエンジンである。

1. Data Loader層: CSVを読み込み、メモリ上の `Dictionary` オブジェクトへ展開する。この際、文字列としての制約条件(”ASAP”等)を、Projectの定数(`pjTaskConstraintType`)へ変換する。
2. Mapping Layer: 外部の識別子と、Project内のタスクを紐づけるロジック。
3. Execution Engine層: Projectのタスクを一度だけ走査し、Dictionaryに存在するデータのみを高速に適用する。

3. 実装:プロダクション・グレードのコード例

このコードを動かすには、VBAエディタの「ツール」→「参照設定」から `Microsoft Scripting Runtime` にチェックを入れておく必要がある。これは、高速なDictionary操作に不可欠だ。

Option Explicit

' --- 定数定義:外部データの文字列とProject定数のマッピング ---
' メンテナンス性を高めるため、Enumではなく、Dictionaryによる動的マッピングを推奨する
' なぜなら、外部仕様変更(例:CSVの文言変更)にコード本体を汚さず対応できるからだ。

Private Type ConstraintData
    ConstraintType As Long
    ConstraintDate As Date
    HasDate As Boolean
End Type

''' <summary>
''' 制約条件インポート・メインエンジン
''' </summary>
Public Sub ImportConstraintsFromCSV()
    Dim csvPath As String
    csvPath = "C:\Temp\TaskConstraints.csv" ' 本来はFileDialog等で選択させるべき
    
    Dim dictMap As Scripting.Dictionary
    Set dictMap = New Scripting.Dictionary
    
    On Error GoTo ErrorHandler
    
    ' 1. Data Loading Phase: CSVをメモリ(Dictionary)へ高速展開
    Debug.Print "Phase 1: Loading CSV data..."
    LoadCSVToDictionary csvPath, dictMap
    
    ' 2. Execution Phase: Projectタスクへの適用
    Debug.Print "Phase 2: Applying constraints to Project..."
    ApplyConstraints dictMap
    
    MsgBox "インポートが正常に完了しました。", vbInformation
    Exit Sub

ErrorHandler:
    MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
End Sub

''' <summary>
''' CSVを読み込み、Dictionaryに格納する。
''' Key: WBSコード (String), Value: ConstraintData (Custom Type)
''' </summary>
Private Sub LoadCSVToDictionary(ByVal filePath As String, ByRef dict As Scripting.Dictionary)
    Dim fileNo As Integer
    Dim lineStr As String
    Dim parts() As String
    Dim wbsKey As String
    Dim typeStr As String
    Dim dateStr As String
    Dim cData As ConstraintData
    
    fileNo = FreeFile
    Open filePath For Input As #fileNo
    
    ' ヘッダーをスキップ
    If Not EOF(fileNo) Then Line Input #fileNo, lineStr
    
    Do While Not EOF(fileNo)
        Line Input #fileNo, lineStr
        parts = Split(lineStr, ",") ' カンマ区切りを想定
        
        ' CSV構成: WBS, ConstraintType, ConstraintDate
        ' 例: 1.1, ASAP, 
        ' 例: 1.2, StartNoEarlierThan, 2023/12/01
        
        wbsKey = Trim(parts(0))
        typeStr = Trim(parts(1))
        dateStr = Trim(parts(2))
        
        ' 制約タイプの変換ロジック
        cData.ConstraintType = MapConstraintType(typeStr)
        
        ' 日付の有無判定
        If dateStr <> "" Then
            cData.ConstraintDate = CDate(dateStr)
            cData.HasDate = True
        Else
            cData.HasDate = False
        End If
        
        ' Dictionaryへの格納 (Key: WBS)
        If Not dict.Exists(wbsKey) Then
            dict.Add wbsKey, cData
        End If
    Loop
    
    Close #fileNo
End Sub

''' <summary>
''' Projectのタスクを走査し、Dictionaryに基づき制約を適用する。
''' </summary>
Private Sub ApplyConstraints(ByRef dict As Scripting.Dictionary)
    Dim t As Task
    Dim wbsKey As String
    Dim cData As ConstraintData
    Dim matchCount As Long: matchCount = 0
    
    ' Projectのタスクを一度だけループする。計算量 O(N)
    For Each t In ActiveProject.Tasks
        If Not t Is Nothing Then
            ' ここではタスクの「WBS」をキーとして照合する
            ' WBSは一意であることが保証されているため、極めて堅牢
            wbsKey = t.WBS
            
            If dict.Exists(wbsKey) Then
                cData = dict(wbsKey)
                
                ' プロパティの適用
                t.ConstraintType = cData.ConstraintType
                If cData.HasDate Then
                    t.ConstraintDate = cData.ConstraintDate
                End If
                
                matchCount = matchCount + 1
            End If
        End If
    Next t
    
    Debug.Print "Applied " & matchCount & " constraints."
End Sub

''' <summary>
''' 文字列をMS Projectの定数へ変換する。
''' </summary>
Private Function MapConstraintType(ByVal typeStr As String) As Long
    Select Case UCase(typeStr)
        Case "ASAP": MapConstraintType = pjTaskConstraintNone
        Case "ALAP": MapConstraintType = pjTaskConstraintAsLateAsPossible
        Case "SNET": MapConstraintType = pjTaskConstraintStartNoEarlierThan
        Case "SFN":  MapConstraintType = pjTaskConstraintStartNoFinish
        Case "FNT":  MapConstraintType = pjTaskConstraintFinishNoEarlierThan
        Case Else
            ' 不明なタイプはデフォルト(ASAP)とするか、エラーを投げる設計にする
            MapConstraintType = pjTaskConstraintNone
    End Select
End Function

4. アーキテクトの視点:運用のための「極限の知見」

このコードを実務に投入する際、単に「動く」こと以上に考慮すべき点が3つある。

① 識別子の「一意性」への執着

上記のコードでは `t.WBS` をキーにしている。これは極めて賢明な判断だ。Task Nameはユーザーが容易に変更できるが、WBSは構造に依存した一意のIDである。もし、あなたの組織がWBSではなく独自の「タスクID」を管理しているなら、その値をProjectの「テキストカスタムフィールド」に格納しておき、それをキーとして照合せよ。「変わる可能性があるもの」をキーにするな。

② エラーハンドリングの階層化

上記のコードでは簡略化しているが、実務では `MapConstraintType` で未知の文字列が来た場合に、単にデフォルト値を返すのではなく、「どのWBSが不正なデータを持っていたか」をログに出力する仕組みが必要だ。データの不備を隠蔽することは、後工程でのスケジュール崩壊を招く。

③ メモリ管理とオブジェクトのライフサイクル

`Scripting.Dictionary` は高速だが、大規模なデータを扱う場合はメモリを消費する。関数を抜ける際に `Set dict = Nothing` を明示的に行う習慣をつけろ。また、Projectのタスクオブジェクトを操作する際は、`If Not t Is Nothing` のチェックを忘れるな。削除されたタスクや、空の行がループに含まれるだけで、コードは即座にクラッシュする。

結論

優れた自動化エンジニアは、コードの行数ではなく、「データの流れ」と「計算の複雑性」を設計する。
CSVを読み込み、マッピングし、適用する。この一連のプロセスを、データ構造(Dictionary)によって最適化することで、あなたのツールは「スクリプト」から「システム」へと昇華する。

次にコードを書くときは、自問自答してほしい。
「このループの中で、重い検索を行っていないか?」と。

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