[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/includes/diff/ -> engine.php (source)

   1  <?php
   2  /**
   3  *
   4  * @package diff
   5  * @version $Id$
   6  * @copyright (c) 2006 phpBB Group
   7  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
   8  *
   9  */
  10  
  11  /**
  12  * @ignore
  13  */
  14  if (!defined('IN_PHPBB'))
  15  {
  16      exit;
  17  }
  18  
  19  /**
  20  * Code from pear.php.net, Text_Diff-1.1.0 package
  21  * http://pear.php.net/package/Text_Diff/ (native engine)
  22  *
  23  * Modified by phpBB Group to meet our coding standards
  24  * and being able to integrate into phpBB
  25  *
  26  * Class used internally by Text_Diff to actually compute the diffs. This
  27  * class is implemented using native PHP code.
  28  *
  29  * The algorithm used here is mostly lifted from the perl module
  30  * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
  31  * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
  32  *
  33  * More ideas are taken from: http://www.ics.uci.edu/~eppstein/161/960229.html
  34  *
  35  * Some ideas (and a bit of code) are taken from analyze.c, of GNU
  36  * diffutils-2.7, which can be found at:
  37  * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
  38  *
  39  * Some ideas (subdivision by NCHUNKS > 2, and some optimizations) are from
  40  * Geoffrey T. Dairiki <dairiki@dairiki.org>. The original PHP version of this
  41  * code was written by him, and is used/adapted with his permission.
  42  *
  43  * Copyright 2004-2008 The Horde Project (http://www.horde.org/)
  44  *
  45  * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
  46  * @package diff
  47  *
  48  * @access private
  49  */
  50  class diff_engine
  51  {
  52      /**
  53      * If set to true we trim all lines before we compare them. This ensures that sole space/tab changes do not trigger diffs.
  54      */
  55      var $skip_whitespace_changes = true;
  56  
  57  	function diff(&$from_lines, &$to_lines, $preserve_cr = true)
  58      {
  59          // Remove empty lines...
  60          // If preserve_cr is true, we basically only change \r\n and bare \r to \n to get the same carriage returns for both files
  61          // If it is false, we try to only use \n once per line and ommit all empty lines to be able to get a proper data diff
  62  
  63          if (is_array($from_lines))
  64          {
  65              $from_lines = implode("\n", $from_lines);
  66          }
  67  
  68          if (is_array($to_lines))
  69          {
  70              $to_lines = implode("\n", $to_lines);
  71          }
  72  
  73          if ($preserve_cr)
  74          {
  75              $from_lines = explode("\n", str_replace("\r", "\n", str_replace("\r\n", "\n", $from_lines)));
  76              $to_lines = explode("\n", str_replace("\r", "\n", str_replace("\r\n", "\n", $to_lines)));
  77          }
  78          else
  79          {
  80              $from_lines = explode("\n", preg_replace('#[\n\r]+#', "\n", $from_lines));
  81              $to_lines = explode("\n", preg_replace('#[\n\r]+#', "\n", $to_lines));
  82          }
  83  
  84          $n_from = sizeof($from_lines);
  85          $n_to = sizeof($to_lines);
  86  
  87          $this->xchanged = $this->ychanged = $this->xv = $this->yv = $this->xind = $this->yind = array();
  88          unset($this->seq, $this->in_seq, $this->lcs);
  89  
  90          // Skip leading common lines.
  91          for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++)
  92          {
  93              if (trim($from_lines[$skip]) !== trim($to_lines[$skip]))
  94              {
  95                  break;
  96              }
  97              $this->xchanged[$skip] = $this->ychanged[$skip] = false;
  98          }
  99  
 100          // Skip trailing common lines.
 101          $xi = $n_from;
 102          $yi = $n_to;
 103  
 104          for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++)
 105          {
 106              if (trim($from_lines[$xi]) !== trim($to_lines[$yi]))
 107              {
 108                  break;
 109              }
 110              $this->xchanged[$xi] = $this->ychanged[$yi] = false;
 111          }
 112  
 113          // Ignore lines which do not exist in both files.
 114          for ($xi = $skip; $xi < $n_from - $endskip; $xi++)
 115          {
 116              if ($this->skip_whitespace_changes) $xhash[trim($from_lines[$xi])] = 1; else $xhash[$from_lines[$xi]] = 1;
 117          }
 118  
 119          for ($yi = $skip; $yi < $n_to - $endskip; $yi++)
 120          {
 121              $line = ($this->skip_whitespace_changes) ? trim($to_lines[$yi]) : $to_lines[$yi];
 122  
 123              if (($this->ychanged[$yi] = empty($xhash[$line])))
 124              {
 125                  continue;
 126              }
 127              $yhash[$line] = 1;
 128              $this->yv[] = $line;
 129              $this->yind[] = $yi;
 130          }
 131  
 132          for ($xi = $skip; $xi < $n_from - $endskip; $xi++)
 133          {
 134              $line = ($this->skip_whitespace_changes) ? trim($from_lines[$xi]) : $from_lines[$xi];
 135  
 136              if (($this->xchanged[$xi] = empty($yhash[$line])))
 137              {
 138                  continue;
 139              }
 140              $this->xv[] = $line;
 141              $this->xind[] = $xi;
 142          }
 143  
 144          // Find the LCS.
 145          $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
 146  
 147          // Merge edits when possible.
 148          if ($this->skip_whitespace_changes)
 149          {
 150              $from_lines_clean = array_map('trim', $from_lines);
 151              $to_lines_clean = array_map('trim', $to_lines);
 152  
 153              $this->_shift_boundaries($from_lines_clean, $this->xchanged, $this->ychanged);
 154              $this->_shift_boundaries($to_lines_clean, $this->ychanged, $this->xchanged);
 155  
 156              unset($from_lines_clean, $to_lines_clean);
 157          }
 158          else
 159          {
 160              $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
 161              $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
 162          }
 163  
 164          // Compute the edit operations.
 165          $edits = array();
 166          $xi = $yi = 0;
 167  
 168          while ($xi < $n_from || $yi < $n_to)
 169          {
 170              // Skip matching "snake".
 171              $copy = array();
 172  
 173              while ($xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi])
 174              {
 175                  $copy[] = $from_lines[$xi++];
 176                  $yi++;
 177              }
 178  
 179              if ($copy)
 180              {
 181                  $edits[] = new diff_op_copy($copy);
 182              }
 183  
 184              // Find deletes & adds.
 185              $delete = array();
 186              while ($xi < $n_from && $this->xchanged[$xi])
 187              {
 188                  $delete[] = $from_lines[$xi++];
 189              }
 190  
 191              $add = array();
 192              while ($yi < $n_to && $this->ychanged[$yi])
 193              {
 194                  $add[] = $to_lines[$yi++];
 195              }
 196  
 197              if ($delete && $add)
 198              {
 199                  $edits[] = new diff_op_change($delete, $add);
 200              }
 201              else if ($delete)
 202              {
 203                  $edits[] = new diff_op_delete($delete);
 204              }
 205              else if ($add)
 206              {
 207                  $edits[] = new diff_op_add($add);
 208              }
 209          }
 210  
 211          return $edits;
 212      }
 213  
 214      /**
 215      * Divides the Largest Common Subsequence (LCS) of the sequences (XOFF,
 216      * XLIM) and (YOFF, YLIM) into NCHUNKS approximately equally sized segments.
 217      *
 218      * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an array of
 219      * NCHUNKS+1 (X, Y) indexes giving the diving points between sub
 220      * sequences.  The first sub-sequence is contained in (X0, X1), (Y0, Y1),
 221      * the second in (X1, X2), (Y1, Y2) and so on.  Note that (X0, Y0) ==
 222      * (XOFF, YOFF) and (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
 223      *
 224      * This function assumes that the first lines of the specified portions of
 225      * the two files do not match, and likewise that the last lines do not
 226      * match.  The caller must trim matching lines from the beginning and end
 227      * of the portions it is going to specify.
 228      */
 229  	function _diag($xoff, $xlim, $yoff, $ylim, $nchunks)
 230      {
 231          $flip = false;
 232  
 233          if ($xlim - $xoff > $ylim - $yoff)
 234          {
 235              // Things seems faster (I'm not sure I understand why) when the shortest sequence is in X.
 236              $flip = true;
 237              list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
 238          }
 239  
 240          if ($flip)
 241          {
 242              for ($i = $ylim - 1; $i >= $yoff; $i--)
 243              {
 244                  $ymatches[$this->xv[$i]][] = $i;
 245              }
 246          }
 247          else
 248          {
 249              for ($i = $ylim - 1; $i >= $yoff; $i--)
 250              {
 251                  $ymatches[$this->yv[$i]][] = $i;
 252              }
 253          }
 254  
 255          $this->lcs = 0;
 256          $this->seq[0]= $yoff - 1;
 257          $this->in_seq = array();
 258          $ymids[0] = array();
 259  
 260          $numer = $xlim - $xoff + $nchunks - 1;
 261          $x = $xoff;
 262  
 263          for ($chunk = 0; $chunk < $nchunks; $chunk++)
 264          {
 265              if ($chunk > 0)
 266              {
 267                  for ($i = 0; $i <= $this->lcs; $i++)
 268                  {
 269                      $ymids[$i][$chunk - 1] = $this->seq[$i];
 270                  }
 271              }
 272  
 273              $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $chunk) / $nchunks);
 274  
 275              for (; $x < $x1; $x++)
 276              {
 277                  $line = $flip ? $this->yv[$x] : $this->xv[$x];
 278                  if (empty($ymatches[$line]))
 279                  {
 280                      continue;
 281                  }
 282                  $matches = $ymatches[$line];
 283  
 284                  reset($matches);
 285                  while (list(, $y) = each($matches))
 286                  {
 287                      if (empty($this->in_seq[$y]))
 288                      {
 289                          $k = $this->_lcs_pos($y);
 290                          $ymids[$k] = $ymids[$k - 1];
 291                          break;
 292                      }
 293                  }
 294  
 295                  // no reset() here
 296                  while (list(, $y) = each($matches))
 297                  {
 298                      if ($y > $this->seq[$k - 1])
 299                      {
 300                          // Optimization: this is a common case: next match is just replacing previous match.
 301                          $this->in_seq[$this->seq[$k]] = false;
 302                          $this->seq[$k] = $y;
 303                          $this->in_seq[$y] = 1;
 304                      }
 305                      else if (empty($this->in_seq[$y]))
 306                      {
 307                          $k = $this->_lcs_pos($y);
 308                          $ymids[$k] = $ymids[$k - 1];
 309                      }
 310                  }
 311              }
 312          }
 313  
 314          $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
 315          $ymid = $ymids[$this->lcs];
 316  
 317          for ($n = 0; $n < $nchunks - 1; $n++)
 318          {
 319              $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
 320              $y1 = $ymid[$n] + 1;
 321              $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
 322          }
 323          $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
 324  
 325          return array($this->lcs, $seps);
 326      }
 327  
 328  	function _lcs_pos($ypos)
 329      {
 330          $end = $this->lcs;
 331  
 332          if ($end == 0 || $ypos > $this->seq[$end])
 333          {
 334              $this->seq[++$this->lcs] = $ypos;
 335              $this->in_seq[$ypos] = 1;
 336              return $this->lcs;
 337          }
 338  
 339          $beg = 1;
 340          while ($beg < $end)
 341          {
 342              $mid = (int)(($beg + $end) / 2);
 343              if ($ypos > $this->seq[$mid])
 344              {
 345                  $beg = $mid + 1;
 346              }
 347              else
 348              {
 349                  $end = $mid;
 350              }
 351          }
 352  
 353          $this->in_seq[$this->seq[$end]] = false;
 354          $this->seq[$end] = $ypos;
 355          $this->in_seq[$ypos] = 1;
 356  
 357          return $end;
 358      }
 359  
 360      /**
 361      * Finds LCS of two sequences.
 362      *
 363      * The results are recorded in the vectors $this->{x,y}changed[], by
 364      * storing a 1 in the element for each line that is an insertion or
 365      * deletion (ie. is not in the LCS).
 366      *
 367      * The subsequence of file 0 is (XOFF, XLIM) and likewise for file 1.
 368      *
 369      * Note that XLIM, YLIM are exclusive bounds.  All line numbers are
 370      * origin-0 and discarded lines are not counted.
 371      */
 372  	function _compareseq($xoff, $xlim, $yoff, $ylim)
 373      {
 374          // Slide down the bottom initial diagonal.
 375          while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff])
 376          {
 377              ++$xoff;
 378              ++$yoff;
 379          }
 380  
 381          // Slide up the top initial diagonal.
 382          while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1])
 383          {
 384              --$xlim;
 385              --$ylim;
 386          }
 387  
 388          if ($xoff == $xlim || $yoff == $ylim)
 389          {
 390              $lcs = 0;
 391          }
 392          else
 393          {
 394              // This is ad hoc but seems to work well.
 395              // $nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
 396              // $nchunks = max(2,min(8,(int)$nchunks));
 397              $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
 398              list($lcs, $seps) = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
 399          }
 400  
 401          if ($lcs == 0)
 402          {
 403              // X and Y sequences have no common subsequence: mark all changed.
 404              while ($yoff < $ylim)
 405              {
 406                  $this->ychanged[$this->yind[$yoff++]] = 1;
 407              }
 408  
 409              while ($xoff < $xlim)
 410              {
 411                  $this->xchanged[$this->xind[$xoff++]] = 1;
 412              }
 413          }
 414          else
 415          {
 416              // Use the partitions to split this problem into subproblems.
 417              reset($seps);
 418              $pt1 = $seps[0];
 419  
 420              while ($pt2 = next($seps))
 421              {
 422                  $this->_compareseq($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
 423                  $pt1 = $pt2;
 424              }
 425          }
 426      }
 427  
 428      /**
 429      * Adjusts inserts/deletes of identical lines to join changes as much as possible.
 430      *
 431      * We do something when a run of changed lines include a line at one end
 432      * and has an excluded, identical line at the other.  We are free to
 433      * choose which identical line is included. 'compareseq' usually chooses
 434      * the one at the beginning, but usually it is cleaner to consider the
 435      * following identical line to be the "change".
 436      *
 437      * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
 438      */
 439  	function _shift_boundaries($lines, &$changed, $other_changed)
 440      {
 441          $i = 0;
 442          $j = 0;
 443  
 444          $len = sizeof($lines);
 445          $other_len = sizeof($other_changed);
 446  
 447          while (1)
 448          {
 449              // Scan forward to find the beginning of another run of
 450              // changes. Also keep track of the corresponding point in the other file.
 451              //
 452              // Throughout this code, $i and $j are adjusted together so that
 453              // the first $i elements of $changed and the first $j elements of
 454              // $other_changed both contain the same number of zeros (unchanged lines).
 455              //
 456              // Furthermore, $j is always kept so that $j == $other_len or $other_changed[$j] == false.
 457              while ($j < $other_len && $other_changed[$j])
 458              {
 459                  $j++;
 460              }
 461  
 462              while ($i < $len && ! $changed[$i])
 463              {
 464                  $i++;
 465                  $j++;
 466  
 467                  while ($j < $other_len && $other_changed[$j])
 468                  {
 469                      $j++;
 470                  }
 471              }
 472  
 473              if ($i == $len)
 474              {
 475                  break;
 476              }
 477  
 478              $start = $i;
 479  
 480              // Find the end of this run of changes.
 481              while (++$i < $len && $changed[$i])
 482              {
 483                  continue;
 484              }
 485  
 486              do
 487              {
 488                  // Record the length of this run of changes, so that we can later determine whether the run has grown.
 489                  $runlength = $i - $start;
 490  
 491                  // Move the changed region back, so long as the previous unchanged line matches the last changed one.
 492                  // This merges with previous changed regions.
 493                  while ($start > 0 && $lines[$start - 1] == $lines[$i - 1])
 494                  {
 495                      $changed[--$start] = 1;
 496                      $changed[--$i] = false;
 497  
 498                      while ($start > 0 && $changed[$start - 1])
 499                      {
 500                          $start--;
 501                      }
 502  
 503                      while ($other_changed[--$j])
 504                      {
 505                          continue;
 506                      }
 507                  }
 508  
 509                  // Set CORRESPONDING to the end of the changed run, at the last point where it corresponds to a changed run in the
 510                  // other file. CORRESPONDING == LEN means no such point has been found.
 511                  $corresponding = $j < $other_len ? $i : $len;
 512  
 513                  // Move the changed region forward, so long as the first changed line matches the following unchanged one.
 514                  // This merges with following changed regions.
 515                  // Do this second, so that if there are no merges, the changed region is moved forward as far as possible.
 516                  while ($i < $len && $lines[$start] == $lines[$i])
 517                  {
 518                      $changed[$start++] = false;
 519                      $changed[$i++] = 1;
 520  
 521                      while ($i < $len && $changed[$i])
 522                      {
 523                          $i++;
 524                      }
 525  
 526                      $j++;
 527                      if ($j < $other_len && $other_changed[$j])
 528                      {
 529                          $corresponding = $i;
 530                          while ($j < $other_len && $other_changed[$j])
 531                          {
 532                              $j++;
 533                          }
 534                      }
 535                  }
 536              }
 537              while ($runlength != $i - $start);
 538  
 539              // If possible, move the fully-merged run of changes back to a corresponding run in the other file.
 540              while ($corresponding < $i)
 541              {
 542                  $changed[--$start] = 1;
 543                  $changed[--$i] = 0;
 544  
 545                  while ($other_changed[--$j])
 546                  {
 547                      continue;
 548                  }
 549              }
 550          }
 551      }
 552  }
 553  
 554  ?>


Generated: Wed Oct 2 15:03:47 2013 Cross-referenced by PHPXref 0.7.1