Differ.php (7189B)
1 <?php 2 /* 3 * This file is part of the Diff package. 4 * 5 * (c) Sebastian Bergmann <sebastian@phpunit.de> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11 namespace SebastianBergmann\Diff; 12 13 use SebastianBergmann\Diff\LCS\LongestCommonSubsequence; 14 use SebastianBergmann\Diff\LCS\TimeEfficientImplementation; 15 use SebastianBergmann\Diff\LCS\MemoryEfficientImplementation; 16 17 /** 18 * Diff implementation. 19 * 20 * @package Diff 21 * @author Sebastian Bergmann <sebastian@phpunit.de> 22 * @author Kore Nordmann <mail@kore-nordmann.de> 23 * @copyright Sebastian Bergmann <sebastian@phpunit.de> 24 * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License 25 * @link http://www.github.com/sebastianbergmann/diff 26 */ 27 class Differ 28 { 29 /** 30 * @var string 31 */ 32 private $header; 33 34 /** 35 * @param string $header 36 */ 37 public function __construct($header = "--- Original\n+++ New\n") 38 { 39 $this->header = $header; 40 } 41 42 /** 43 * Returns the diff between two arrays or strings as string. 44 * 45 * @param array|string $from 46 * @param array|string $to 47 * @param LongestCommonSubsequence $lcs 48 * @return string 49 */ 50 public function diff($from, $to, LongestCommonSubsequence $lcs = null) 51 { 52 if (!is_array($from) && !is_string($from)) { 53 $from = (string) $from; 54 } 55 56 if (!is_array($to) && !is_string($to)) { 57 $to = (string) $to; 58 } 59 60 $buffer = $this->header; 61 $diff = $this->diffToArray($from, $to, $lcs); 62 63 $inOld = false; 64 $i = 0; 65 $old = array(); 66 67 foreach ($diff as $line) { 68 if ($line[1] === 0 /* OLD */) { 69 if ($inOld === false) { 70 $inOld = $i; 71 } 72 } elseif ($inOld !== false) { 73 if (($i - $inOld) > 5) { 74 $old[$inOld] = $i - 1; 75 } 76 77 $inOld = false; 78 } 79 80 ++$i; 81 } 82 83 $start = isset($old[0]) ? $old[0] : 0; 84 $end = count($diff); 85 86 if ($tmp = array_search($end, $old)) { 87 $end = $tmp; 88 } 89 90 $newChunk = true; 91 92 for ($i = $start; $i < $end; $i++) { 93 if (isset($old[$i])) { 94 $buffer .= "\n"; 95 $newChunk = true; 96 $i = $old[$i]; 97 } 98 99 if ($newChunk) { 100 $buffer .= "@@ @@\n"; 101 $newChunk = false; 102 } 103 104 if ($diff[$i][1] === 1 /* ADDED */) { 105 $buffer .= '+' . $diff[$i][0] . "\n"; 106 } elseif ($diff[$i][1] === 2 /* REMOVED */) { 107 $buffer .= '-' . $diff[$i][0] . "\n"; 108 } else { 109 $buffer .= ' ' . $diff[$i][0] . "\n"; 110 } 111 } 112 113 return $buffer; 114 } 115 116 /** 117 * Returns the diff between two arrays or strings as array. 118 * 119 * Each array element contains two elements: 120 * - [0] => string $token 121 * - [1] => 2|1|0 122 * 123 * - 2: REMOVED: $token was removed from $from 124 * - 1: ADDED: $token was added to $from 125 * - 0: OLD: $token is not changed in $to 126 * 127 * @param array|string $from 128 * @param array|string $to 129 * @param LongestCommonSubsequence $lcs 130 * @return array 131 */ 132 public function diffToArray($from, $to, LongestCommonSubsequence $lcs = null) 133 { 134 preg_match_all('(\r\n|\r|\n)', $from, $fromMatches); 135 preg_match_all('(\r\n|\r|\n)', $to, $toMatches); 136 137 if (is_string($from)) { 138 $from = preg_split('(\r\n|\r|\n)', $from); 139 } 140 141 if (is_string($to)) { 142 $to = preg_split('(\r\n|\r|\n)', $to); 143 } 144 145 $start = array(); 146 $end = array(); 147 $fromLength = count($from); 148 $toLength = count($to); 149 $length = min($fromLength, $toLength); 150 151 for ($i = 0; $i < $length; ++$i) { 152 if ($from[$i] === $to[$i]) { 153 $start[] = $from[$i]; 154 unset($from[$i], $to[$i]); 155 } else { 156 break; 157 } 158 } 159 160 $length -= $i; 161 162 for ($i = 1; $i < $length; ++$i) { 163 if ($from[$fromLength - $i] === $to[$toLength - $i]) { 164 array_unshift($end, $from[$fromLength - $i]); 165 unset($from[$fromLength - $i], $to[$toLength - $i]); 166 } else { 167 break; 168 } 169 } 170 171 if ($lcs === null) { 172 $lcs = $this->selectLcsImplementation($from, $to); 173 } 174 175 $common = $lcs->calculate(array_values($from), array_values($to)); 176 $diff = array(); 177 178 if (isset($fromMatches[0]) && $toMatches[0] && 179 count($fromMatches[0]) === count($toMatches[0]) && 180 $fromMatches[0] !== $toMatches[0]) { 181 $diff[] = array( 182 '#Warning: Strings contain different line endings!', 0 183 ); 184 } 185 186 foreach ($start as $token) { 187 $diff[] = array($token, 0 /* OLD */); 188 } 189 190 reset($from); 191 reset($to); 192 193 foreach ($common as $token) { 194 while ((($fromToken = reset($from)) !== $token)) { 195 $diff[] = array(array_shift($from), 2 /* REMOVED */); 196 } 197 198 while ((($toToken = reset($to)) !== $token)) { 199 $diff[] = array(array_shift($to), 1 /* ADDED */); 200 } 201 202 $diff[] = array($token, 0 /* OLD */); 203 204 array_shift($from); 205 array_shift($to); 206 } 207 208 while (($token = array_shift($from)) !== null) { 209 $diff[] = array($token, 2 /* REMOVED */); 210 } 211 212 while (($token = array_shift($to)) !== null) { 213 $diff[] = array($token, 1 /* ADDED */); 214 } 215 216 foreach ($end as $token) { 217 $diff[] = array($token, 0 /* OLD */); 218 } 219 220 return $diff; 221 } 222 223 /** 224 * @param array $from 225 * @param array $to 226 * @return LongestCommonSubsequence 227 */ 228 private function selectLcsImplementation(array $from, array $to) 229 { 230 // We do not want to use the time-efficient implementation if its memory 231 // footprint will probably exceed this value. Note that the footprint 232 // calculation is only an estimation for the matrix and the LCS method 233 // will typically allocate a bit more memory than this. 234 $memoryLimit = 100 * 1024 * 1024; 235 236 if ($this->calculateEstimatedFootprint($from, $to) > $memoryLimit) { 237 return new MemoryEfficientImplementation; 238 } 239 240 return new TimeEfficientImplementation; 241 } 242 243 /** 244 * Calculates the estimated memory footprint for the DP-based method. 245 * 246 * @param array $from 247 * @param array $to 248 * @return integer 249 */ 250 private function calculateEstimatedFootprint(array $from, array $to) 251 { 252 $itemSize = PHP_INT_SIZE == 4 ? 76 : 144; 253 254 return $itemSize * pow(min(count($from), count($to)), 2); 255 } 256 }