fisharebest /
algorithm
| 1 | <?php |
||||
| 2 | |||||
| 3 | namespace Fisharebest\Algorithm; |
||||
| 4 | |||||
| 5 | /** |
||||
| 6 | * @author Greg Roach <[email protected]> |
||||
| 7 | * @copyright (c) 2021 Greg Roach <[email protected]> |
||||
| 8 | * @license GPL-3.0+ |
||||
| 9 | * |
||||
| 10 | * This program is free software: you can redistribute it and/or modify |
||||
| 11 | * it under the terms of the GNU General Public License as published by |
||||
| 12 | * the Free Software Foundation, either version 3 of the License, or |
||||
| 13 | * (at your option) any later version. |
||||
| 14 | * This program is distributed in the hope that it will be useful, |
||||
| 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
| 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
| 17 | * GNU General Public License for more details. |
||||
| 18 | * You should have received a copy of the GNU General Public License |
||||
| 19 | * along with this program. If not, see <https://www.gnu.org/licenses>. |
||||
| 20 | */ |
||||
| 21 | |||||
| 22 | /** |
||||
| 23 | * Class MyersDiff - find the shortest edit sequence to transform one string into another. |
||||
| 24 | * |
||||
| 25 | * Based on "An O(ND) Difference Algorithm and Its Variations" by Eugene W Myers. |
||||
| 26 | * |
||||
| 27 | * http://www.xmailserver.org/diff2.pdf |
||||
| 28 | * http://www.codeproject.com/Articles/42279/Investigating-Myers-diff-algorithm-Part-of |
||||
| 29 | */ |
||||
| 30 | class MyersDiff |
||||
| 31 | { |
||||
| 32 | /** Instruction to delete a token which only appears in the first sequence */ |
||||
| 33 | const DELETE = -1; |
||||
| 34 | |||||
| 35 | /** Instruction to keep a token which is common to both sequences */ |
||||
| 36 | const KEEP = 0; |
||||
| 37 | |||||
| 38 | /** Instruction to insert a token which only appears in the last sequence */ |
||||
| 39 | const INSERT = 1; |
||||
| 40 | |||||
| 41 | /** |
||||
| 42 | * Backtrack through the intermediate results to extract the "snakes" that |
||||
| 43 | * are visited on the chosen "D-path". |
||||
| 44 | * |
||||
| 45 | * @param string[] $v_save Intermediate results |
||||
| 46 | * @param int $x End position |
||||
| 47 | * @param int $y End position |
||||
| 48 | * |
||||
| 49 | * @return list<array{int, int}> |
||||
|
0 ignored issues
–
show
|
|||||
| 50 | */ |
||||
| 51 | private function extractSnakes(array $v_save, $x, $y) |
||||
| 52 | { |
||||
| 53 | $snakes = array(); |
||||
| 54 | for ($d = count($v_save) - 1; $x >= 0 && $y >= 0; $d--) { |
||||
| 55 | array_unshift($snakes, array($x, $y)); |
||||
| 56 | |||||
| 57 | $v = $v_save[$d]; |
||||
| 58 | $k = $x - $y; |
||||
| 59 | |||||
| 60 | if ($k === -$d || $k !== $d && $v[$k - 1] < $v[$k + 1]) { |
||||
| 61 | $k_prev = $k + 1; |
||||
| 62 | } else { |
||||
| 63 | $k_prev = $k - 1; |
||||
| 64 | } |
||||
| 65 | |||||
| 66 | $x = $v[$k_prev]; |
||||
| 67 | $y = $x - $k_prev; |
||||
| 68 | } |
||||
| 69 | |||||
| 70 | return $snakes; |
||||
| 71 | } |
||||
| 72 | |||||
| 73 | /** |
||||
| 74 | * Convert a list of "snakes" into a set of insert/keep/delete instructions. |
||||
| 75 | * |
||||
| 76 | * @template T |
||||
| 77 | * |
||||
| 78 | * @param list<array{int, int}> $snakes Common subsequences |
||||
| 79 | * @param list<T> $a First sequence |
||||
| 80 | * @param list<T> $b Second sequence |
||||
| 81 | * |
||||
| 82 | * @return list<array{T, -1|0|1}> - pairs of token and edit (-1 for delete, 0 for keep, +1 for insert) |
||||
| 83 | */ |
||||
| 84 | private function formatSolution(array $snakes, array $a, array $b) |
||||
| 85 | { |
||||
| 86 | $solution = array(); |
||||
| 87 | $x = 0; |
||||
| 88 | $y = 0; |
||||
| 89 | foreach ($snakes as $snake) { |
||||
| 90 | // Horizontals |
||||
| 91 | while ($snake[0] - $snake[1] > $x - $y) { |
||||
| 92 | $solution[] = array($a[$x], self::DELETE); |
||||
| 93 | $x++; |
||||
| 94 | } |
||||
| 95 | // Verticals |
||||
| 96 | while ($snake[0] - $snake[1] < $x - $y) { |
||||
| 97 | $solution[] = array($b[$y], self::INSERT); |
||||
| 98 | $y++; |
||||
| 99 | } |
||||
| 100 | // Diagonals |
||||
| 101 | while ($x < $snake[0]) { |
||||
| 102 | $solution[] = array($a[$x], self::KEEP); |
||||
| 103 | $x++; |
||||
| 104 | $y++; |
||||
| 105 | } |
||||
| 106 | } |
||||
| 107 | |||||
| 108 | return $solution; |
||||
|
0 ignored issues
–
show
|
|||||
| 109 | } |
||||
| 110 | |||||
| 111 | /** |
||||
| 112 | * Calculate the shortest edit sequence to convert $x into $y. |
||||
| 113 | * |
||||
| 114 | * @template T |
||||
| 115 | * |
||||
| 116 | * @param list<T> $a - List of values to compare. |
||||
| 117 | * @param list<T> $b - List of values to compare. |
||||
| 118 | * @param (callable(T, T): bool)|null $compare - comparison function for tokens, or NULL to use === comparison. |
||||
|
0 ignored issues
–
show
|
|||||
| 119 | * |
||||
| 120 | * @return list<array{T, -1|0|1}> - pairs of token and edit (-1 for delete, 0 for keep, +1 for insert) |
||||
| 121 | */ |
||||
| 122 | public function calculate(array $a, array $b, $compare = null) |
||||
| 123 | { |
||||
| 124 | if ($compare === null) { |
||||
| 125 | $compare = function ($x, $y) { |
||||
| 126 | return $x === $y; |
||||
| 127 | }; |
||||
| 128 | } |
||||
| 129 | |||||
| 130 | // The algorithm uses array keys numbered from zero. |
||||
| 131 | $n = count($a); |
||||
| 132 | $m = count($b); |
||||
| 133 | $a = array_values($a); |
||||
| 134 | $b = array_values($b); |
||||
| 135 | $max = $m + $n; |
||||
| 136 | |||||
| 137 | // Keep a copy of $v after each iteration of $d. |
||||
| 138 | $v_save = array(); |
||||
| 139 | |||||
| 140 | // Find the shortest "D-path". |
||||
| 141 | $v = array(1 => 0); |
||||
| 142 | for ($d = 0; $d <= $max; $d++) { |
||||
| 143 | // Examine all possible "K-lines" for this "D-path". |
||||
| 144 | for ($k = -$d; $k <= $d; $k += 2) { |
||||
| 145 | if ($k === -$d || $k !== $d && $v[$k - 1] < $v[$k + 1]) { |
||||
| 146 | // Move down. |
||||
| 147 | $x = $v[$k + 1]; |
||||
| 148 | } else { |
||||
| 149 | // Move right. |
||||
| 150 | $x = $v[$k - 1] + 1; |
||||
| 151 | } |
||||
| 152 | // Derive Y from X. |
||||
| 153 | $y = $x - $k; |
||||
| 154 | // Follow the diagonal. |
||||
| 155 | while ($x < $n && $y < $m && $compare($a[$x], $b[$y])) { |
||||
| 156 | $x++; |
||||
| 157 | $y++; |
||||
| 158 | } |
||||
| 159 | // Just store X, as we can calculate Y (from X + K). |
||||
| 160 | $v[$k] = $x; |
||||
| 161 | $v_save[$d] = $v; |
||||
| 162 | // Solution found? |
||||
| 163 | if ($x === $n && $y === $m) { |
||||
| 164 | break 2; |
||||
| 165 | } |
||||
| 166 | } |
||||
| 167 | } |
||||
| 168 | |||||
| 169 | // Extract the solution by back-tracking through the saved results. |
||||
| 170 | $snakes = $this->extractSnakes($v_save, $n, $m); |
||||
| 171 | |||||
| 172 | // Format the snakes as a set of instructions. |
||||
| 173 | return $this->formatSolution($snakes, $a, $b); |
||||
|
0 ignored issues
–
show
$snakes of type Fisharebest\Algorithm\list is incompatible with the type array expected by parameter $snakes of Fisharebest\Algorithm\MyersDiff::formatSolution().
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
| 174 | } |
||||
| 175 | } |
||||
| 176 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths