Private/Get-MyersOperation.ps1

#Requires -Version 5.1

function Get-MyersOperation {
    <#
    .SYNOPSIS
        Myers 法で、変更前から変更後への最短の編集操作(Same / Deleted / Added)を算出する。
    .DESCRIPTION
        Changed(変更)の判定は行いません。削除行と追加行の対応付けは Get-DiffAlignment が行います。
    .PARAMETER Left
        変更前の行 ID の配列(ConvertTo-LineId の Left)。空の配列も受け取ります。
    .PARAMETER Right
        変更後の行 ID の配列(ConvertTo-LineId の Right)。空の配列も受け取ります。
    .OUTPUTS
        [System.Collections.Generic.List[hashtable]]
        @{ Kind = 'Same'|'Deleted'|'Added'; LeftIndex = 変更前の添字 or $null; RightIndex = 変更後の添字 or $null }
        の List。変更前と変更後のすべての要素を、先頭から順に 1 回ずつ含みます。
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Generic.List[hashtable]])]
    param(
        [Parameter(Mandatory)] [AllowEmptyCollection()] [int[]]$Left,
        [Parameter(Mandatory)] [AllowEmptyCollection()] [int[]]$Right
    )

    $leftLength = $Left.Length
    $rightLength = $Right.Length
    $ops = [System.Collections.Generic.List[hashtable]]::new()

    # 片側が空なら、Myers 法を実行する必要はない。
    if ($leftLength -eq 0 -and $rightLength -eq 0) { return $ops }
    if ($leftLength -eq 0) {
        for ($rightIndex = 0; $rightIndex -lt $rightLength; $rightIndex++) {
            $ops.Add(@{ Kind = 'Added'; LeftIndex = $null; RightIndex = $rightIndex })
        }
        return $ops
    }
    if ($rightLength -eq 0) {
        for ($leftIndex = 0; $leftIndex -lt $leftLength; $leftIndex++) {
            $ops.Add(@{ Kind = 'Deleted'; LeftIndex = $leftIndex; RightIndex = $null })
        }
        return $ops
    }

    # 変数名と論文(Myers, "An O(ND) Difference Algorithm and Its Variations")の記号の対応:
    # $leftLength = N / $rightLength = M / $editCount = D / $diagonal = k /
    # $leftPos = x / $rightPos = y / $furthest = V(対角線ごとの最も遠い x)
    # 添字の中の算術は必ず括弧で囲む。$table[$row + 1, $column] は $table[$row + (1, $column)] と解釈され、
    # 配列の連結になって失敗する。
    $maxEditCount = $leftLength + $rightLength
    $size = 2 * $maxEditCount + 2
    $offset = $maxEditCount
    $furthest = New-Object -TypeName 'int[]' -ArgumentList $size
    # 各段の V を保存する。保存しないと経路を復元できない。
    # PowerShell のループで複製すると D に比例して遅くなるため、[Array]::Copy で複製する。
    $trace = [System.Collections.Generic.List[int[]]]::new()

    $shortestEditCount = -1
    for ($editCount = 0; $editCount -le $maxEditCount; $editCount++) {
        $furthestCopy = New-Object -TypeName 'int[]' -ArgumentList $size
        [Array]::Copy($furthest, $furthestCopy, $size)
        $trace.Add($furthestCopy)

        for ($diagonal = -$editCount; $diagonal -le $editCount; $diagonal += 2) {
            $slot = $offset + $diagonal
            if ($diagonal -ceq -$editCount -or ($diagonal -cne $editCount -and $furthest[($slot - 1)] -lt $furthest[($slot + 1)])) {
                $leftPos = $furthest[($slot + 1)]
            }
            else {
                $leftPos = $furthest[($slot - 1)] + 1
            }
            $rightPos = $leftPos - $diagonal
            # 一致が続く間、対角線方向に進む。
            while ($leftPos -lt $leftLength -and $rightPos -lt $rightLength -and $Left[$leftPos] -ceq $Right[$rightPos]) {
                $leftPos++
                $rightPos++
            }
            $furthest[$slot] = $leftPos
            if ($leftPos -ge $leftLength -and $rightPos -ge $rightLength) { $shortestEditCount = $editCount; break }
        }
        if ($shortestEditCount -ge 0) { break }
    }

    # --- 経路の復元(末尾から先頭へ)---
    $leftPos = $leftLength
    $rightPos = $rightLength
    for ($editCount = $shortestEditCount; $editCount -gt 0; $editCount--) {
        $previousFurthest = $trace[$editCount]
        $diagonal = $leftPos - $rightPos
        $slot = $offset + $diagonal
        if ($diagonal -ceq -$editCount -or ($diagonal -cne $editCount -and $previousFurthest[($slot - 1)] -lt $previousFurthest[($slot + 1)])) {
            $previousDiagonal = $diagonal + 1
        }
        else {
            $previousDiagonal = $diagonal - 1
        }
        $previousLeftPos = $previousFurthest[($offset + $previousDiagonal)]
        $previousRightPos = $previousLeftPos - $previousDiagonal

        while ($leftPos -gt $previousLeftPos -and $rightPos -gt $previousRightPos) {
            $ops.Add(@{ Kind = 'Same'; LeftIndex = ($leftPos - 1); RightIndex = ($rightPos - 1) })
            $leftPos--
            $rightPos--
        }
        if ($leftPos -gt $previousLeftPos) { $ops.Add(@{ Kind = 'Deleted'; LeftIndex = ($leftPos - 1); RightIndex = $null }) }
        else { $ops.Add(@{ Kind = 'Added'; LeftIndex = $null; RightIndex = ($rightPos - 1) }) }
        $leftPos = $previousLeftPos
        $rightPos = $previousRightPos
    }
    while ($leftPos -gt 0 -and $rightPos -gt 0) {
        $ops.Add(@{ Kind = 'Same'; LeftIndex = ($leftPos - 1); RightIndex = ($rightPos - 1) })
        $leftPos--
        $rightPos--
    }

    $ops.Reverse()
    return $ops
}