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 ) # --- 1. 相手側に一致が無い要素を除外する --- # 相手側に一致が無い要素は必ず Deleted か Added になるため、先に除外して Myers 法の計算量を減らす。 # 除外しても最短の編集は変わらず、残る要素は相手側にも必ず残るため、片側だけが空になることはない。 $leftSet = [System.Collections.Generic.HashSet[int]]::new($Left) $rightSet = [System.Collections.Generic.HashSet[int]]::new($Right) $keptLeft = [System.Collections.Generic.List[int]]::new() $keptRight = [System.Collections.Generic.List[int]]::new() for ($leftIndex = 0; $leftIndex -lt $Left.Length; $leftIndex++) { if ($rightSet.Contains($Left[$leftIndex])) { $keptLeft.Add($leftIndex) } } for ($rightIndex = 0; $rightIndex -lt $Right.Length; $rightIndex++) { if ($leftSet.Contains($Right[$rightIndex])) { $keptRight.Add($rightIndex) } } $leftIds = New-Object -TypeName 'int[]' -ArgumentList $keptLeft.Count for ($keptIndex = 0; $keptIndex -lt $keptLeft.Count; $keptIndex++) { $leftIds[$keptIndex] = $Left[$keptLeft[$keptIndex]] } $rightIds = New-Object -TypeName 'int[]' -ArgumentList $keptRight.Count for ($keptIndex = 0; $keptIndex -lt $keptRight.Count; $keptIndex++) { $rightIds[$keptIndex] = $Right[$keptRight[$keptIndex]] } # --- 2. 残った要素を Myers 法で比較する --- # 添字は残った要素の中での位置。両方とも空なら、手数 0 で終了する。 $leftLength = $leftIds.Length $rightLength = $rightIds.Length $keptOps = [System.Collections.Generic.List[hashtable]]::new() # 変数名と論文(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 $leftIds[$leftPos] -ceq $rightIds[$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) { $keptOps.Add(@{ Kind = 'Same'; LeftIndex = ($leftPos - 1); RightIndex = ($rightPos - 1) }) $leftPos-- $rightPos-- } if ($leftPos -gt $previousLeftPos) { $keptOps.Add(@{ Kind = 'Deleted'; LeftIndex = ($leftPos - 1); RightIndex = $null }) } else { $keptOps.Add(@{ Kind = 'Added'; LeftIndex = $null; RightIndex = ($rightPos - 1) }) } $leftPos = $previousLeftPos $rightPos = $previousRightPos } while ($leftPos -gt 0 -and $rightPos -gt 0) { $keptOps.Add(@{ Kind = 'Same'; LeftIndex = ($leftPos - 1); RightIndex = ($rightPos - 1) }) $leftPos-- $rightPos-- } $keptOps.Reverse() # --- 3. 除外した要素を、元の位置に Deleted と Added として戻す --- # 残った要素の操作を元の添字に変換し、その手前にある除外した要素を先に出力する。 $ops = [System.Collections.Generic.List[hashtable]]::new() $leftNext = 0 $rightNext = 0 foreach ($keptOp in $keptOps) { if ($null -ne $keptOp.LeftIndex) { $leftTarget = $keptLeft[$keptOp.LeftIndex] while ($leftNext -lt $leftTarget) { $ops.Add(@{ Kind = 'Deleted'; LeftIndex = $leftNext; RightIndex = $null }); $leftNext++ } } if ($null -ne $keptOp.RightIndex) { $rightTarget = $keptRight[$keptOp.RightIndex] while ($rightNext -lt $rightTarget) { $ops.Add(@{ Kind = 'Added'; LeftIndex = $null; RightIndex = $rightNext }); $rightNext++ } } switch -CaseSensitive ($keptOp.Kind) { 'Same' { $ops.Add(@{ Kind = 'Same'; LeftIndex = $leftNext; RightIndex = $rightNext }); $leftNext++; $rightNext++ } 'Deleted' { $ops.Add(@{ Kind = 'Deleted'; LeftIndex = $leftNext; RightIndex = $null }); $leftNext++ } 'Added' { $ops.Add(@{ Kind = 'Added'; LeftIndex = $null; RightIndex = $rightNext }); $rightNext++ } } } while ($leftNext -lt $Left.Length) { $ops.Add(@{ Kind = 'Deleted'; LeftIndex = $leftNext; RightIndex = $null }); $leftNext++ } while ($rightNext -lt $Right.Length) { $ops.Add(@{ Kind = 'Added'; LeftIndex = $null; RightIndex = $rightNext }); $rightNext++ } return $ops } |