Text_Diff
[ class tree: Text_Diff ] [ index: Text_Diff ] [ all elements ]

Source for file native.php

Documentation is available at native.php

  1. <?php
  2. /**
  3.  * Class used internally by Text_Diff to actually compute the diffs.
  4.  *
  5.  * This class is implemented using native PHP code.
  6.  *
  7.  * The algorithm used here is mostly lifted from the perl module
  8.  * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
  9.  * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
  10.  *
  11.  * More ideas are taken from: http://www.ics.uci.edu/~eppstein/161/960229.html
  12.  *
  13.  * Some ideas (and a bit of code) are taken from analyze.c, of GNU
  14.  * diffutils-2.7, which can be found at:
  15.  * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
  16.  *
  17.  * Some ideas (subdivision by NCHUNKS > 2, and some optimizations) are from
  18.  * Geoffrey T. Dairiki <dairiki@dairiki.org>. The original PHP version of this
  19.  * code was written by him, and is used/adapted with his permission.
  20.  *
  21.  * $Horde: framework/Text_Diff/Diff/Engine/native.php,v 1.7.2.5 2009/01/06 15:23:41 jan Exp $
  22.  *
  23.  * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
  24.  *
  25.  * See the enclosed file COPYING for license information (LGPL). If you did
  26.  * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
  27.  *
  28.  * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
  29.  * @package Text_Diff
  30.  */
  31.  
  32.     function diff($from_lines$to_lines)
  33.     {
  34.         array_walk($from_linesarray('Text_Diff''trimNewlines'));
  35.         array_walk($to_linesarray('Text_Diff''trimNewlines'));
  36.  
  37.         $n_from count($from_lines);
  38.         $n_to count($to_lines);
  39.  
  40.         $this->xchanged $this->ychanged = array();
  41.         $this->xv $this->yv = array();
  42.         $this->xind $this->yind = array();
  43.         unset($this->seq);
  44.         unset($this->in_seq);
  45.         unset($this->lcs);
  46.  
  47.         // Skip leading common lines.
  48.         for ($skip = 0; $skip $n_from && $skip $n_to$skip++{
  49.             if ($from_lines[$skip!== $to_lines[$skip]{
  50.                 break;
  51.             }
  52.             $this->xchanged[$skip$this->ychanged[$skip= false;
  53.         }
  54.  
  55.         // Skip trailing common lines.
  56.         $xi $n_from$yi $n_to;
  57.         for ($endskip = 0; --$xi $skip && --$yi $skip$endskip++{
  58.             if ($from_lines[$xi!== $to_lines[$yi]{
  59.                 break;
  60.             }
  61.             $this->xchanged[$xi$this->ychanged[$yi= false;
  62.         }
  63.  
  64.         // Ignore lines which do not exist in both files.
  65.         for ($xi $skip$xi $n_from $endskip$xi++{
  66.             $xhash[$from_lines[$xi]] = 1;
  67.         }
  68.         for ($yi $skip$yi $n_to $endskip$yi++{
  69.             $line $to_lines[$yi];
  70.             if (($this->ychanged[$yi= empty($xhash[$line]))) {
  71.                 continue;
  72.             }
  73.             $yhash[$line= 1;
  74.             $this->yv[$line;
  75.             $this->yind[$yi;
  76.         }
  77.         for ($xi $skip$xi $n_from $endskip$xi++{
  78.             $line $from_lines[$xi];
  79.             if (($this->xchanged[$xi= empty($yhash[$line]))) {
  80.                 continue;
  81.             }
  82.             $this->xv[$line;
  83.             $this->xind[$xi;
  84.         }
  85.  
  86.         // Find the LCS.
  87.         $this->_compareseq(0count($this->xv)0count($this->yv));
  88.  
  89.         // Merge edits when possible.
  90.         $this->_shiftBoundaries($from_lines$this->xchanged$this->ychanged);
  91.         $this->_shiftBoundaries($to_lines$this->ychanged$this->xchanged);
  92.  
  93.         // Compute the edit operations.
  94.         $edits = array();
  95.         $xi $yi = 0;
  96.         while ($xi $n_from || $yi $n_to{
  97.             assert($yi $n_to || $this->xchanged[$xi]);
  98.             assert($xi $n_from || $this->ychanged[$yi]);
  99.  
  100.             // Skip matching "snake".
  101.             $copy = array();
  102.             while ($xi $n_from && $yi $n_to
  103.                    && !$this->xchanged[$xi&& !$this->ychanged[$yi]{
  104.                 $copy[$from_lines[$xi++];
  105.                 ++$yi;
  106.             }
  107.             if ($copy{
  108.                 $edits[= new Text_Diff_Op_copy($copy);
  109.             }
  110.  
  111.             // Find deletes & adds.
  112.             $delete = array();
  113.             while ($xi $n_from && $this->xchanged[$xi]{
  114.                 $delete[$from_lines[$xi++];
  115.             }
  116.  
  117.             $add = array();
  118.             while ($yi $n_to && $this->ychanged[$yi]{
  119.                 $add[$to_lines[$yi++];
  120.             }
  121.  
  122.             if ($delete && $add{
  123.                 $edits[= new Text_Diff_Op_change($delete$add);
  124.             elseif ($delete{
  125.                 $edits[= new Text_Diff_Op_delete($delete);
  126.             elseif ($add{
  127.                 $edits[= new Text_Diff_Op_add($add);
  128.             }
  129.         }
  130.  
  131.         return $edits;
  132.     }
  133.  
  134.     /**
  135.      * Divides the Largest Common Subsequence (LCS) of the sequences (XOFF,
  136.      * XLIM) and (YOFF, YLIM) into NCHUNKS approximately equally sized
  137.      * segments.
  138.      *
  139.      * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an array of
  140.      * NCHUNKS+1 (X, Y) indexes giving the diving points between sub
  141.      * sequences.  The first sub-sequence is contained in (X0, X1), (Y0, Y1),
  142.      * the second in (X1, X2), (Y1, Y2) and so on.  Note that (X0, Y0) ==
  143.      * (XOFF, YOFF) and (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
  144.      *
  145.      * This function assumes that the first lines of the specified portions of
  146.      * the two files do not match, and likewise that the last lines do not
  147.      * match.  The caller must trim matching lines from the beginning and end
  148.      * of the portions it is going to specify.
  149.      */
  150.     function _diag ($xoff$xlim$yoff$ylim$nchunks)
  151.     {
  152.         $flip = false;
  153.  
  154.         if ($xlim $xoff $ylim $yoff{
  155.             /* Things seems faster (I'm not sure I understand why) when the
  156.              * shortest sequence is in X. */
  157.             $flip = true;
  158.             list ($xoff$xlim$yoff$ylim)
  159.                 = array($yoff$ylim$xoff$xlim);
  160.         }
  161.  
  162.         if ($flip{
  163.             for ($i $ylim - 1; $i >= $yoff$i--{
  164.                 $ymatches[$this->xv[$i]][$i;
  165.             }
  166.         else {
  167.             for ($i $ylim - 1; $i >= $yoff$i--{
  168.                 $ymatches[$this->yv[$i]][$i;
  169.             }
  170.         }
  171.  
  172.         $this->lcs = 0;
  173.         $this->seq[0]$yoff - 1;
  174.         $this->in_seq = array();
  175.         $ymids[0= array();
  176.  
  177.         $numer $xlim $xoff $nchunks - 1;
  178.         $x $xoff;
  179.         for ($chunk = 0; $chunk $nchunks$chunk++{
  180.             if ($chunk > 0{
  181.                 for ($i = 0; $i <= $this->lcs$i++{
  182.                     $ymids[$i][$chunk - 1$this->seq[$i];
  183.                 }
  184.             }
  185.  
  186.             $x1 $xoff + (int)(($numer ($xlim $xoff$chunk$nchunks);
  187.             for ($x $x1$x++{
  188.                 $line $flip $this->yv[$x$this->xv[$x];
  189.                 if (empty($ymatches[$line])) {
  190.                     continue;
  191.                 }
  192.                 $matches $ymatches[$line];
  193.                 reset($matches);
  194.                 while (list($yeach($matches)) {
  195.                     if (empty($this->in_seq[$y])) {
  196.                         $k $this->_lcsPos($y);
  197.                         assert($k > 0);
  198.                         $ymids[$k$ymids[$k - 1];
  199.                         break;
  200.                     }
  201.                 }
  202.                 while (list($yeach($matches)) {
  203.                     if ($y $this->seq[$k - 1]{
  204.                         assert($y <= $this->seq[$k]);
  205.                         /* Optimization: this is a common case: next match is
  206.                          * just replacing previous match. */
  207.                         $this->in_seq[$this->seq[$k]] = false;
  208.                         $this->seq[$k$y;
  209.                         $this->in_seq[$y= 1;
  210.                     elseif (empty($this->in_seq[$y])) {
  211.                         $k $this->_lcsPos($y);
  212.                         assert($k > 0);
  213.                         $ymids[$k$ymids[$k - 1];
  214.                     }
  215.                 }
  216.             }
  217.         }
  218.  
  219.         $seps[$flip ? array($yoff$xoff: array($xoff$yoff);
  220.         $ymid $ymids[$this->lcs];
  221.         for ($n = 0; $n $nchunks - 1; $n++{
  222.             $x1 $xoff + (int)(($numer ($xlim $xoff$n$nchunks);
  223.             $y1 $ymid[$n+ 1;
  224.             $seps[$flip ? array($y1$x1: array($x1$y1);
  225.         }
  226.         $seps[$flip ? array($ylim$xlim: array($xlim$ylim);
  227.  
  228.         return array($this->lcs$seps);
  229.     }
  230.  
  231.     function _lcsPos($ypos)
  232.     {
  233.         $end $this->lcs;
  234.         if ($end == 0 || $ypos $this->seq[$end]{
  235.             $this->seq[++$this->lcs$ypos;
  236.             $this->in_seq[$ypos= 1;
  237.             return $this->lcs;
  238.         }
  239.  
  240.         $beg = 1;
  241.         while ($beg $end{
  242.             $mid = (int)(($beg $end/ 2);
  243.             if ($ypos $this->seq[$mid]{
  244.                 $beg $mid + 1;
  245.             else {
  246.                 $end $mid;
  247.             }
  248.         }
  249.  
  250.         assert($ypos != $this->seq[$end]);
  251.  
  252.         $this->in_seq[$this->seq[$end]] = false;
  253.         $this->seq[$end$ypos;
  254.         $this->in_seq[$ypos= 1;
  255.         return $end;
  256.     }
  257.  
  258.     /**
  259.      * Finds LCS of two sequences.
  260.      *
  261.      * The results are recorded in the vectors $this->{x,y}changed[], by
  262.      * storing a 1 in the element for each line that is an insertion or
  263.      * deletion (ie. is not in the LCS).
  264.      *
  265.      * The subsequence of file 0 is (XOFF, XLIM) and likewise for file 1.
  266.      *
  267.      * Note that XLIM, YLIM are exclusive bounds.  All line numbers are
  268.      * origin-0 and discarded lines are not counted.
  269.      */
  270.     function _compareseq ($xoff$xlim$yoff$ylim)
  271.     {
  272.         /* Slide down the bottom initial diagonal. */
  273.         while ($xoff $xlim && $yoff $ylim
  274.                && $this->xv[$xoff== $this->yv[$yoff]{
  275.             ++$xoff;
  276.             ++$yoff;
  277.         }
  278.  
  279.         /* Slide up the top initial diagonal. */
  280.         while ($xlim $xoff && $ylim $yoff
  281.                && $this->xv[$xlim - 1== $this->yv[$ylim - 1]{
  282.             --$xlim;
  283.             --$ylim;
  284.         }
  285.  
  286.         if ($xoff == $xlim || $yoff == $ylim{
  287.             $lcs = 0;
  288.         else {
  289.             /* This is ad hoc but seems to work well.  $nchunks =
  290.              * sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5); $nchunks =
  291.              * max(2,min(8,(int)$nchunks)); */
  292.             $nchunks min(7$xlim $xoff$ylim $yoff+ 1;
  293.             list($lcs$seps)
  294.                 = $this->_diag($xoff$xlim$yoff$ylim$nchunks);
  295.         }
  296.  
  297.         if ($lcs == 0{
  298.             /* X and Y sequences have no common subsequence: mark all
  299.              * changed. */
  300.             while ($yoff $ylim{
  301.                 $this->ychanged[$this->yind[$yoff++]] = 1;
  302.             }
  303.             while ($xoff $xlim{
  304.                 $this->xchanged[$this->xind[$xoff++]] = 1;
  305.             }
  306.         else {
  307.             /* Use the partitions to split this problem into subproblems. */
  308.             reset($seps);
  309.             $pt1 $seps[0];
  310.             while ($pt2 next($seps)) {
  311.                 $this->_compareseq ($pt1[0]$pt2[0]$pt1[1]$pt2[1]);
  312.                 $pt1 $pt2;
  313.             }
  314.         }
  315.     }
  316.  
  317.     /**
  318.      * Adjusts inserts/deletes of identical lines to join changes as much as
  319.      * possible.
  320.      *
  321.      * We do something when a run of changed lines include a line at one end
  322.      * and has an excluded, identical line at the other.  We are free to
  323.      * choose which identical line is included.  `compareseq' usually chooses
  324.      * the one at the beginning, but usually it is cleaner to consider the
  325.      * following identical line to be the "change".
  326.      *
  327.      * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
  328.      */
  329.     function _shiftBoundaries($lines&$changed$other_changed)
  330.     {
  331.         $i = 0;
  332.         $j = 0;
  333.  
  334.         assert('count($lines) == count($changed)');
  335.         $len count($lines);
  336.         $other_len count($other_changed);
  337.  
  338.         while (1{
  339.             /* Scan forward to find the beginning of another run of
  340.              * changes. Also keep track of the corresponding point in the
  341.              * other file.
  342.              *
  343.              * Throughout this code, $i and $j are adjusted together so that
  344.              * the first $i elements of $changed and the first $j elements of
  345.              * $other_changed both contain the same number of zeros (unchanged
  346.              * lines).
  347.              *
  348.              * Furthermore, $j is always kept so that $j == $other_len or
  349.              * $other_changed[$j] == false. */
  350.             while ($j $other_len && $other_changed[$j]{
  351.                 $j++;
  352.             }
  353.  
  354.             while ($i $len && $changed[$i]{
  355.                 assert('$j < $other_len && ! $other_changed[$j]');
  356.                 $i++; $j++;
  357.                 while ($j $other_len && $other_changed[$j]{
  358.                     $j++;
  359.                 }
  360.             }
  361.  
  362.             if ($i == $len{
  363.                 break;
  364.             }
  365.  
  366.             $start $i;
  367.  
  368.             /* Find the end of this run of changes. */
  369.             while (++$i $len && $changed[$i]{
  370.                 continue;
  371.             }
  372.  
  373.             do {
  374.                 /* Record the length of this run of changes, so that we can
  375.                  * later determine whether the run has grown. */
  376.                 $runlength $i $start;
  377.  
  378.                 /* Move the changed region back, so long as the previous
  379.                  * unchanged line matches the last changed one.  This merges
  380.                  * with previous changed regions. */
  381.                 while ($start > 0 && $lines[$start - 1== $lines[$i - 1]{
  382.                     $changed[--$start= 1;
  383.                     $changed[--$i= false;
  384.                     while ($start > 0 && $changed[$start - 1]{
  385.                         $start--;
  386.                     }
  387.                     assert('$j > 0');
  388.                     while ($other_changed[--$j]{
  389.                         continue;
  390.                     }
  391.                     assert('$j >= 0 && !$other_changed[$j]');
  392.                 }
  393.  
  394.                 /* Set CORRESPONDING to the end of the changed run, at the
  395.                  * last point where it corresponds to a changed run in the
  396.                  * other file. CORRESPONDING == LEN means no such point has
  397.                  * been found. */
  398.                 $corresponding $j $other_len $i $len;
  399.  
  400.                 /* Move the changed region forward, so long as the first
  401.                  * changed line matches the following unchanged one.  This
  402.                  * merges with following changed regions.  Do this second, so
  403.                  * that if there are no merges, the changed region is moved
  404.                  * forward as far as possible. */
  405.                 while ($i $len && $lines[$start== $lines[$i]{
  406.                     $changed[$start++= false;
  407.                     $changed[$i++= 1;
  408.                     while ($i $len && $changed[$i]{
  409.                         $i++;
  410.                     }
  411.  
  412.                     assert('$j < $other_len && ! $other_changed[$j]');
  413.                     $j++;
  414.                     if ($j $other_len && $other_changed[$j]{
  415.                         $corresponding $i;
  416.                         while ($j $other_len && $other_changed[$j]{
  417.                             $j++;
  418.                         }
  419.                     }
  420.                 }
  421.             while ($runlength != $i $start);
  422.  
  423.             /* If possible, move the fully-merged run of changes back to a
  424.              * corresponding run in the other file. */
  425.             while ($corresponding $i{
  426.                 $changed[--$start= 1;
  427.                 $changed[--$i= 0;
  428.                 assert('$j > 0');
  429.                 while ($other_changed[--$j]{
  430.                     continue;
  431.                 }
  432.                 assert('$j >= 0 && !$other_changed[$j]');
  433.             }
  434.         }
  435.     }
  436.  
  437. }

Documentation generated on Mon, 11 Mar 2019 16:03:26 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.