Differ.php 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. <?php declare(strict_types=1);
  2. namespace PhpParser\Internal;
  3. /**
  4. * Implements the Myers diff algorithm.
  5. *
  6. * Myers, Eugene W. "An O (ND) difference algorithm and its variations."
  7. * Algorithmica 1.1 (1986): 251-266.
  8. *
  9. * @template T
  10. * @internal
  11. */
  12. class Differ {
  13. /** @var callable(T, T): bool */
  14. private $isEqual;
  15. /**
  16. * Create differ over the given equality relation.
  17. *
  18. * @param callable(T, T): bool $isEqual Equality relation
  19. */
  20. public function __construct(callable $isEqual) {
  21. $this->isEqual = $isEqual;
  22. }
  23. /**
  24. * Calculate diff (edit script) from $old to $new.
  25. *
  26. * @param T[] $old Original array
  27. * @param T[] $new New array
  28. *
  29. * @return DiffElem[] Diff (edit script)
  30. */
  31. public function diff(array $old, array $new): array {
  32. $old = \array_values($old);
  33. $new = \array_values($new);
  34. list($trace, $x, $y) = $this->calculateTrace($old, $new);
  35. return $this->extractDiff($trace, $x, $y, $old, $new);
  36. }
  37. /**
  38. * Calculate diff, including "replace" operations.
  39. *
  40. * If a sequence of remove operations is followed by the same number of add operations, these
  41. * will be coalesced into replace operations.
  42. *
  43. * @param T[] $old Original array
  44. * @param T[] $new New array
  45. *
  46. * @return DiffElem[] Diff (edit script), including replace operations
  47. */
  48. public function diffWithReplacements(array $old, array $new): array {
  49. return $this->coalesceReplacements($this->diff($old, $new));
  50. }
  51. /**
  52. * @param T[] $old
  53. * @param T[] $new
  54. * @return array{array<int, array<int, int>>, int, int}
  55. */
  56. private function calculateTrace(array $old, array $new): array {
  57. $n = \count($old);
  58. $m = \count($new);
  59. $max = $n + $m;
  60. $v = [1 => 0];
  61. $trace = [];
  62. for ($d = 0; $d <= $max; $d++) {
  63. $trace[] = $v;
  64. for ($k = -$d; $k <= $d; $k += 2) {
  65. if ($k === -$d || ($k !== $d && $v[$k - 1] < $v[$k + 1])) {
  66. $x = $v[$k + 1];
  67. } else {
  68. $x = $v[$k - 1] + 1;
  69. }
  70. $y = $x - $k;
  71. while ($x < $n && $y < $m && ($this->isEqual)($old[$x], $new[$y])) {
  72. $x++;
  73. $y++;
  74. }
  75. $v[$k] = $x;
  76. if ($x >= $n && $y >= $m) {
  77. return [$trace, $x, $y];
  78. }
  79. }
  80. }
  81. throw new \Exception('Should not happen');
  82. }
  83. /**
  84. * @param array<int, array<int, int>> $trace
  85. * @param T[] $old
  86. * @param T[] $new
  87. * @return DiffElem[]
  88. */
  89. private function extractDiff(array $trace, int $x, int $y, array $old, array $new): array {
  90. $result = [];
  91. for ($d = \count($trace) - 1; $d >= 0; $d--) {
  92. $v = $trace[$d];
  93. $k = $x - $y;
  94. if ($k === -$d || ($k !== $d && $v[$k - 1] < $v[$k + 1])) {
  95. $prevK = $k + 1;
  96. } else {
  97. $prevK = $k - 1;
  98. }
  99. $prevX = $v[$prevK];
  100. $prevY = $prevX - $prevK;
  101. while ($x > $prevX && $y > $prevY) {
  102. $result[] = new DiffElem(DiffElem::TYPE_KEEP, $old[$x - 1], $new[$y - 1]);
  103. $x--;
  104. $y--;
  105. }
  106. if ($d === 0) {
  107. break;
  108. }
  109. while ($x > $prevX) {
  110. $result[] = new DiffElem(DiffElem::TYPE_REMOVE, $old[$x - 1], null);
  111. $x--;
  112. }
  113. while ($y > $prevY) {
  114. $result[] = new DiffElem(DiffElem::TYPE_ADD, null, $new[$y - 1]);
  115. $y--;
  116. }
  117. }
  118. return array_reverse($result);
  119. }
  120. /**
  121. * Coalesce equal-length sequences of remove+add into a replace operation.
  122. *
  123. * @param DiffElem[] $diff
  124. * @return DiffElem[]
  125. */
  126. private function coalesceReplacements(array $diff): array {
  127. $newDiff = [];
  128. $c = \count($diff);
  129. for ($i = 0; $i < $c; $i++) {
  130. $diffType = $diff[$i]->type;
  131. if ($diffType !== DiffElem::TYPE_REMOVE) {
  132. $newDiff[] = $diff[$i];
  133. continue;
  134. }
  135. $j = $i;
  136. while ($j < $c && $diff[$j]->type === DiffElem::TYPE_REMOVE) {
  137. $j++;
  138. }
  139. $k = $j;
  140. while ($k < $c && $diff[$k]->type === DiffElem::TYPE_ADD) {
  141. $k++;
  142. }
  143. if ($j - $i === $k - $j) {
  144. $len = $j - $i;
  145. for ($n = 0; $n < $len; $n++) {
  146. $newDiff[] = new DiffElem(
  147. DiffElem::TYPE_REPLACE, $diff[$i + $n]->old, $diff[$j + $n]->new
  148. );
  149. }
  150. } else {
  151. for (; $i < $k; $i++) {
  152. $newDiff[] = $diff[$i];
  153. }
  154. }
  155. $i = $k - 1;
  156. }
  157. return $newDiff;
  158. }
  159. }