🔁

[PowerShell]2次元のジャグ配列と多次元配列それぞれに変換できるFunction

に公開

Hello, world、どうもakiGAMEBOY(アキゲームボーイ)です。今回も自己学習のためにPowerShellを使った自作関数の記事となります。

わかりやすく言葉を区別するために少し冗長的な表現になりますが、2次元のジャグ配列、または2次元の多次元配列いずれかのデータから相互変換するため、2つを作成しました。

  1. ジャグ配列 → 多次元配列 に変換するFunction
  2. 多次元配列 → ジャグ配列 に変換するFunction

自作した関数

では、さっそく作成したコードの紹介。

1. ジャグ配列 → 多次元配列 に変換するFunction

ジャグ配列 → 多次元配列 に変換するFunction
Function Convert-JaggedToMulti {
    [CmdletBinding()]
    <#
    .SYNOPSIS
        ジャグ配列(配列の配列)を2次元の多次元配列に変換。
    .DESCRIPTION
        入力されたジャグ配列を分析し、すべての要素を格納できる最小の多次元配列を作成。
        各行の要素数が異なり不足している要素は $null 埋めされる。
    .PARAMETER JaggedArray
        変換対象のジャグ配列(配列が内包された配列)。
    .EXAMPLE
        # テスト用のジャグ配列を作成
        PS> $jagged = @(@(1, 2), @(3, 4, 5), @(6))

        # 関数を実行して多次元配列に変換
        PS> Convert-JaggedToMulti -JaggedArray $jagged
    .OUTPUTS
        [System.Object[,]]
    #>
    param(
        # 多次元配列 → ジャグ配列 で発生した奇妙な挙動にあわせて引数必須オプションをコメントアウト(有効にしても正常動作可能)
        #[Parameter(Mandatory = $true)] 
        [array]$JaggedArray
    )

    # 入力が空の配列の場合は、0x0の多次元配列を返す
    if ($JaggedArray.Length -eq 0) {
        $multiArray = New-Object 'object[,]' 0, 0
        return ,$multiArray
    }

    # 各行の配列の中で、最大の要素数を取得
    $maxColumnCount = ($JaggedArray | Where-Object { $_ -is [System.Array] } | Measure-Object -Property Length -Maximum).Maximum

    # 入力に配列が1つも含まれない場合、$maxColumnCount が $null になるため、0に設定
    if ($null -eq $maxColumnCount) {
        $maxColumnCount = 0
    }

    # 多次元配列を初期化 (ここでの出力は抑制しない。最後に明示的に返すため)
    $multiArray = New-Object 'object[,]' $JaggedArray.Length, $maxColumnCount

    # 要素をコピー
    for ($i = 0; $i -lt $JaggedArray.Length; $i++) {
        if ($JaggedArray[$i] -is [System.Array]) {
            for ($j = 0; $j -lt $JaggedArray[$i].Length; $j++) {
                $multiArray[$i, $j] = $JaggedArray[$i][$j]
                #Write-Debug "`$multiArray[$i, $j]: [$($multiArray[$i, $j])]"
            }
        }
    }
    
    return ,$multiArray
}

2. 多次元配列 → ジャグ配列 に変換するFunction

多次元配列 → ジャグ配列 に変換するFunction
Function Convert-MultiToJagged {
    [CmdletBinding()]
    <#
    .SYNOPSIS
        2次元の多次元配列をジャグ配列(配列の配列)に変換します。
    .DESCRIPTION
        入力された2次元の多次元配列を分析し、ジャグ配列を生成します。
        多次元配列の各行が、ジャグ配列の内部配列に対応します。
        元の配列に含まれる $null 値は、変換後のジャグ配列には含まれません。
    .PARAMETER MultiArray
        変換対象の2次元多次元配列。
    .EXAMPLE
        # テスト用の多次元配列を作成
        PS> $multi = New-Object 'object[,]' 3, 3
        PS> $multi[0,0] = 1; $multi[0,1] = 2
        PS> $multi[1,0] = 3; $multi[1,1] = 4; $multi[1,2] = 5
        PS> $multi[2,0] = 6
        
        # 関数を実行してジャグ配列に変換
        PS> $jagged = Convert-MultiToJagged -MultiArray $multi
    .OUTPUTS
        [System.Object[]]
        各要素が配列であるジャグ配列。
    #>
    param(
        #引数必須で指定していると、なぜかnullが渡されてしまう奇妙な挙動が発生した為、コメントアウト
        #[Parameter(Mandatory = $true)]
        [System.Array]$MultiArray
    )

    # 入力が2次元配列であることを検証
    if ($MultiArray.Rank -ne 2) {
        Write-Error "入力は2次元の多次元配列である必要があります。入力された配列の次元数: $($MultiArray.Rank)"
        return
    }

    # ジャグ配列を格納するための可変長リストを準備
    $jaggedArrayList = [System.Collections.ArrayList]::new()

    # 行と列の数を取得
    $rowCount = $MultiArray.GetLength(0)
    $colCount = $MultiArray.GetLength(1)

    # 各行をループ処理
    for ($i = 0; $i -lt $rowCount; $i++) {
        # 各行の要素を格納するための可変長リストを準備
        $rowList = [System.Collections.ArrayList]::new()

        # 各列をループ処理
        for ($j = 0; $j -lt $colCount; $j++) {
            $element = $MultiArray[$i, $j]
            
            # 要素が $null でない場合のみリストに追加
            if ($null -ne $element) {
                # Add()メソッドの戻り値(追加された要素のインデックス)がパイプラインに出力されるのを防ぐ
                [void]$rowList.Add($element)
                #Write-Debug "`$rowList: [$($rowList)]"
            }
        }

        # 完成した行(配列に変換済み)を全体のジャグ配列リストに追加
        [void]$jaggedArrayList.Add($rowList.ToArray())
        #Write-Debug "`$jaggedArrayList[$i]: [$($jaggedArrayList[$i])]"
    }

    # 最終的な結果(ArrayListを配列に変換したもの)をパイプラインに出力
    # これにより、呼び出し元はジャグ配列として受け取れる
    return ,$jaggedArrayList.ToArray()
}
補足情報:複数要素を持つ配列をreturnすると1次元配列になってしまう現象について

当初、関数内で作成した2次元配列を単純にreturn $multiArrayreturn $jaggedArrayList.ToArray()で戻す方法を選択していました。
すると、なぜか戻り値が1次元配列で戻るという想定外の現状が発生。

調べると、この挙動はPowerShellのパイプライン周りの仕様による原因とのこと。関数内で配列やコレクションをreturnすると、その中身の要素を自動的に1つずつパイプラインに展開(アンロール)されてしまうため、戻り値が1次元配列になってしまうことが判明。

対応方法としては、 単項カンマ演算子,)を戻り値で使用することで解決しました!

Function Convert-AAAAToBBBB {
    [CmdletBinding()]
    param(
        [System.Array]$Array
    )

    # ...省略...

-    return $Array
+    return ,$Array
}

下記が参考文献です。

以上が 補足情報:複数要素を持つ配列をreturnすると1次元配列になってしまう現象について でした。

役立つシチュエーション と 良い点・注意点

今回、作成した2つのPowerShell関数 Convert-JaggedToMultiConvert-MultiToJagged について、具体的な利用シチュエーション と 良い点、注意点をまとめてみました。


これら関数が役立つ具体的なシチュエーション

これらの関数は、PowerShellの柔軟な「ジャグ配列」と、他のシステムで要求されることが多い厳密な「多次元配列」との間のデータ構造のギャップを埋めるために役立つでしょう。

1. Convert-JaggedToMulti(ジャグ配列 → 多次元配列)の利用シーン

  • a. dotNETライブラリやCOMオブジェクトとの連携
    多くのdotNET(.NET)ライブラリや、Microsoft OfficeのCOMオートメーションは、メソッドの引数として厳密な矩形(長方形)の多次元配列を要求されます。

    • シナリオ例:Excelシートへのデータ一括書き込み
      PowerShellでWeb APIから取得したデータは、行ごとに項目数が異なるジャグ配列になることが大半。しかし、ExcelのRange.Valueプロパティにデータを一括で書き込む際は、整然とした多次元配列を渡すことで、セルごとに書き込むより劇的に高速化が可能。

      # 行ごとに要素数が異なるジャグ配列
      $dataFromApi = @(
          @("製品A", "カテゴリ1", 100),
          @("製品B", "カテゴリ2", 150, "セール品"), # 項目数が多い
          @("製品C", "カテゴリ1", 200)
      )
      
      # 矩形の多次元配列に変換
      $multiArrayForExcel = Convert-JaggedToMulti -JaggedArray $dataFromApi
      
      # Excelに一括で高速書き込み
      # ... (Excel COMオブジェクトの処理) ...
      # $range.Value2 = $multiArrayForExcel
      
  • b. 表形式データの整形と安全なアクセス
    異なるソースから収集したデータを行ごとに配列に格納した場合、各行の要素数が不揃いになることがあります。これを整然とした表として扱い、インデックス範囲外エラーを心配せずに特定の列にアクセスしたい場合に役立ちます。

    # 4番目の要素がある行とない行が混在
    $logExtracts = @(
        @("2023-10-27", "INFO", "User 'A' logged in."),
        @("2023-10-27", "ERROR", "Disk full.", "Code:507")
    )
    $multiArrayLogs = Convert-JaggedToMulti -JaggedArray $logExtracts
    
    # これで、$multiArrayLogs[0, 3] は $null となり安全にアクセス可能
    $multiArrayLogs | ForEach-Object { ... }
    

2. Convert-MultiToJagged(多次元配列 → ジャグ配列)の利用シーン

  • a. PowerShellのパイプライン処理との親和性向上
    COMオブジェクトなどから取得した多次元配列は全要素がフラットに展開されてしまうため、PowerShellのパイプライン(|)でうまく扱えない。ジャグ配列に変換することで、Where-ObjectForEach-Objectを使った行単位での柔軟な処理が可能に。

    • シナリオ例:Excelシートから読み込んだデータの加工
      Excelから読み込んだデータを、PowerShellらしいパイプライン処理でフィルタリングしたり、オブジェクトに変換したりと柔軟に。

      # Excelから読み込んだ多次元配列を $multiArrayFromExcel とする
      $jaggedData = Convert-MultiToJagged -MultiArray $multiArrayFromExcel
      
      # パイプラインで行単位の処理が可能に
      $processedData = $jaggedData | Where-Object { $_[1] -eq "カテゴリ1" } `
                                  | ForEach-Object { [PSCustomObject]@{ Name = $_[0]; Price = $_[2] } }
      
  • b. JSONへの変換とエクスポート
    PowerShellのConvertTo-Jsonは、ジャグ配列は期待通りに変換されますが、多次元配列では意図した構造にならないことがあります。データをJSON形式でAPIに送信したり、ファイルに保存したりする前の前処理として実用的です。

    # $multiArray は何らかの処理で得られた多次元配列とする
    $jaggedArray = Convert-MultiToJagged -MultiArray $multiArray
    
    # 期待通りのJSON(配列の配列)に変換できる
    $jaggedArray | ConvertTo-Json -Depth 3 | Out-File "data.json"
    

評価:良い点と注意点

良い点

  1. 実用的な問題解決
    PowerShell単体では扱いづらい「データ構造の不一致」という現実的な問題を解決し、とくに外部システム連携の幅を広げることが可能。
  2. 堅牢性とコードの再利用性
    複雑な変換ロジックを堅牢な関数としてカプセル化することで、メインのスクリプトは本質的な処理に集中でき、コードがクリーンで読みやすい。
  3. PowerShellの弱点の補強
    多次元配列の扱いが苦手というPowerShellの弱点を補い、PowerShellが得意とするパイプライン処理にデータを適合させる「橋渡し」の役割を果たす。

注意点(注意事項・制限事項)

  1. 2次元限定の実装
    これらの関数は2次元配列に特化しており、3次元以上の複雑なデータ構造には対応していません。
    (この関数をベースに再帰処理を活用することで3次元以上の実装も可能だと思われます。必要に応じてカスタマイズしてください!)
  2. パフォーマンスの限界
    非常に巨大な配列(数百万要素など)を扱う場合、新しい配列をメモリ上に生成するため、パフォーマンスの低下やメモリ消費量の増大が懸念。
    しっかりとしたテストが不可欠です。
  3. データ型の汎用化
    Convert-JaggedToMultiは汎用性を重視してobject[,]型の配列を生成します。そのため、後続の処理で厳密な型が必要な場合は、[int]$valueのような型キャストが必要になるケースも。
開発者TIPS:必須パラメーター指定時に発生した奇妙な挙動について

コード内のコメントにも記載している通りConvert-MultiToJaggedのコーディング中、

param(
    [Parameter(Mandatory=$true)]
    [System.Array]$MultiArray
)

というように必須パラメーターを指定したところ、「パラメーターがnullである」という不可解なエラーに遭遇しました。変数が存在することを確認してもエラーが100%再現。

必須パラーメーター指定で発生した奇妙なエラー
PS C:\Users\XXXX> $jagged = Convert-MultiToJagged -MultiArray $multi
Convert-MultiToJagged: Cannot bind argument to parameter 'MultiArray' because it is null.
PS C:\Users\XXXX>

調査しても原因は特定できませんでした。おそらく多次元配列が引数である事と必須パラメーターが有効である2点が起因により発生している稀なエラーだと思われます。

回避方法は必須パラメーターを削除と正常動作しました。不思議ですね。

同じような現象に遭遇した方の参考になれば幸いです。

以上が 開発者TIPS:必須パラメーター指定時に発生した奇妙な挙動について でした。

まとめ

今回作成した2つの関数は、日常的なスクリプトでは出番がないかもしれませんが、PowerShellをより高度なタスク、とくに外部システム連携や複雑なデータ整形に活用する方にとって 「かゆいところに手が届く」便利なツール になったのではないでしょうか。

今後も気になったことを発信していこうと思うので、ぜひフォローしてください!

参考文献

https://qiita.com/hitsumabushi845/items/fe4219ba9daa34f541c2
https://qiita.com/Mount/items/d10ff1eb41617f4b8368

関連記事

ジャグ配列や多次元配列の関連記事

https://zenn.dev/haretokidoki/articles/f79a5bb769973f
https://zenn.dev/haretokidoki/articles/45c5af7cbf7eb8
https://zenn.dev/haretokidoki/articles/c1aeace31dbf48

その他 PowerShellの関連記事

https://haretokidoki-blog.com/pasocon_powershell-startup/
https://zenn.dev/haretokidoki/articles/7e6924ff0cc960
https://zenn.dev/haretokidoki/articles/fb6830f9155de5

GitHubで編集を提案

Discussion