Added diffing.

This commit is contained in:
Jeff Welch 2013-02-07 00:50:01 -05:00
parent 5bd7ac6cfe
commit f4e4284fc1
3 changed files with 616 additions and 2 deletions

325
src/PHP_Exporter/Diff.php Normal file
View File

@ -0,0 +1,325 @@
<?php
/**
* PHP_Exporter
*
* Copyright (c) 2001-2013, Sebastian Bergmann <sebastian@phpunit.de>.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Sebastian Bergmann nor the names of his
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @package PHP_Exporter
* @author Sebastian Bergmann <sebastian@phpunit.de>
* @author Kore Nordmann <mail@kore-nordmann.de>
* @copyright 2001-2013 Sebastian Bergmann <sebastian@phpunit.de>
* @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
* @link http://www.github.com/whatthejeff/php-exporter
*/
namespace PHP_Exporter;
/**
* Diff implementation.
*
* @package PHP_Exporter
* @author Sebastian Bergmann <sebastian@phpunit.de>
* @author Kore Nordmann <mail@kore-nordmann.de>
* @copyright 2001-2013 Sebastian Bergmann <sebastian@phpunit.de>
* @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
* @link http://www.github.com/whatthejeff/php-exporter
*/
class Diff
{
/**
* The original string
*
* @var string
*/
private $from;
/**
* The new string
*
* @var string
*/
private $to;
/**
* The diff header
*
* @var string
*/
private $header = "--- Original\n+++ New\n";
/**
* Constructs a new diff for two given strings
*
* @param mixed $from The original string
* @param mixed $to The new string
* @param mixed $header The diff header
*/
public function __construct($from, $to, $header = '')
{
$this->from = $from;
$this->to = $to;
if($header) {
$this->header = $header;
}
}
/**
* Exports a value into a string.
*
* @return string
* @see PHP_Exporter\Diff::diff
*/
public function __toString()
{
return $this->diff();
}
/**
* Returns the diff between two strings as a string.
*
* @return string
*/
public function diff()
{
if($this->from === $this->to) {
return '';
}
$buffer = $this->header;
$diff = $this->toArray();
$inOld = FALSE;
$i = 0;
$old = array();
foreach ($diff as $line) {
if ($line[1] === 0 /* OLD */) {
if ($inOld === FALSE) {
$inOld = $i;
}
}
else if ($inOld !== FALSE) {
if (($i - $inOld) > 5) {
$old[$inOld] = $i - 1;
}
$inOld = FALSE;
}
++$i;
}
$start = isset($old[0]) ? $old[0] : 0;
$end = count($diff);
if ($tmp = array_search($end, $old)) {
$end = $tmp;
}
$newChunk = TRUE;
for ($i = $start; $i < $end; $i++) {
if (isset($old[$i])) {
$buffer .= "\n";
$newChunk = TRUE;
$i = $old[$i];
}
if ($newChunk) {
$buffer .= "@@ @@\n";
$newChunk = FALSE;
}
if ($diff[$i][1] === 1 /* ADDED */) {
$buffer .= '+' . $diff[$i][0] . "\n";
}
else if ($diff[$i][1] === 2 /* REMOVED */) {
$buffer .= '-' . $diff[$i][0] . "\n";
}
else {
$buffer .= ' ' . $diff[$i][0] . "\n";
}
}
return $buffer;
}
/**
* Returns the diff between two strings as an array.
*
* every array-entry containts two elements:
* - [0] => string $token
* - [1] => 2|1|0
*
* - 2: REMOVED: $token was removed from $from
* - 1: ADDED: $token was added to $from
* - 0: OLD: $token is not changed in $to
*
* @return array
*/
public function toArray()
{
if($this->from === $this->to) {
return array();
}
$from = preg_split('(\r\n|\r|\n)', $this->from);
$to = preg_split('(\r\n|\r|\n)', $this->to);
$start = array();
$end = array();
$fromLength = count($from);
$toLength = count($to);
$length = min($fromLength, $toLength);
for ($i = 0; $i < $length; ++$i) {
if ($from[$i] === $to[$i]) {
$start[] = $from[$i];
unset($from[$i], $to[$i]);
} else {
break;
}
}
$length -= $i;
for ($i = 1; $i < $length; ++$i) {
if ($from[$fromLength - $i] === $to[$toLength - $i]) {
array_unshift($end, $from[$fromLength - $i]);
unset($from[$fromLength - $i], $to[$toLength - $i]);
} else {
break;
}
}
$common = self::longestCommonSubsequence(
array_values($from), array_values($to)
);
$diff = array();
foreach ($start as $token) {
$diff[] = array($token, 0 /* OLD */);
}
reset($from);
reset($to);
foreach ($common as $token) {
while ((($fromToken = reset($from)) !== $token)) {
$diff[] = array(array_shift($from), 2 /* REMOVED */);
}
while ((($toToken = reset($to)) !== $token)) {
$diff[] = array(array_shift($to), 1 /* ADDED */);
}
$diff[] = array($token, 0 /* OLD */);
array_shift($from);
array_shift($to);
}
while (($token = array_shift($from)) !== NULL) {
$diff[] = array($token, 2 /* REMOVED */);
}
while (($token = array_shift($to)) !== NULL) {
$diff[] = array($token, 1 /* ADDED */);
}
foreach ($end as $token) {
$diff[] = array($token, 0 /* OLD */);
}
return $diff;
}
/**
* Calculates the longest common subsequence of two arrays.
*
* @param array $from
* @param array $to
* @return array
*/
protected static function longestCommonSubsequence(array $from, array $to)
{
$common = array();
$matrix = array();
$fromLength = count($from);
$toLength = count($to);
for ($i = 0; $i <= $fromLength; ++$i) {
$matrix[$i][0] = 0;
}
for ($j = 0; $j <= $toLength; ++$j) {
$matrix[0][$j] = 0;
}
for ($i = 1; $i <= $fromLength; ++$i) {
for ($j = 1; $j <= $toLength; ++$j) {
$matrix[$i][$j] = max(
$matrix[$i-1][$j],
$matrix[$i][$j-1],
$from[$i-1] === $to[$j-1] ? $matrix[$i-1][$j-1] + 1 : 0
);
}
}
$i = $fromLength;
$j = $toLength;
while ($i > 0 && $j > 0) {
if ($from[$i-1] === $to[$j-1]) {
array_unshift($common, $from[$i-1]);
--$i;
--$j;
}
else if ($matrix[$i][$j-1] > $matrix[$i-1][$j]) {
--$j;
}
else {
--$i;
}
}
return $common;
}
}

View File

@ -72,7 +72,7 @@ class Exporter
}
/**
* Exports a value into a string
* Exports a value as a string
*
* The output of this method is similar to the output of print_r(), but
* improved in various aspects:
@ -93,7 +93,7 @@ class Exporter
}
/**
* Exports a value into a string.
* Exports a value as a string.
*
* @return string
* @see PHP_Exporter\Exporter::export
@ -257,6 +257,18 @@ class Exporter
return $this->export();
}
/**
* Gets a diff of the current value and a given value.
*
* @param mixed $value The value to diff with the current value
* @param mixed $header The diff header
* @return PHP_Exporter\Diff
*/
public function diff($value, $header = '')
{
return new Diff((string)$this, (string)new self($value), $header);
}
/**
* Converts an object to an array containing all of its private, protected
* and public properties.

View File

@ -0,0 +1,277 @@
<?php
/**
* PHP_Exporter
*
* Copyright (c) 2001-2013, Sebastian Bergmann <sebastian@phpunit.de>.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Sebastian Bergmann nor the names of his
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @package PHP_Exporter
* @author Sebastian Bergmann <sebastian@phpunit.de>
* @copyright 2001-2013 Sebastian Bergmann <sebastian@phpunit.de>
* @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
* @link http://www.github.com/whatthejeff/php-exporter
*/
use PHP_Exporter\Diff;
/**
*
*
* @package PHPUnit
* @author Sebastian Bergmann <sebastian@phpunit.de>
* @copyright 2001-2013 Sebastian Bergmann <sebastian@phpunit.de>
* @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
* @link http://www.github.com/whatthejeff/php-exporter
*/
class DiffTest extends PHPUnit_Framework_TestCase
{
const REMOVED = 2;
const ADDED = 1;
const OLD = 0;
/**
* @covers PHP_Exporter\Diff::diff
*/
public function testComparisonErrorMessage()
{
$this->assertEquals(
"--- Original\n+++ New\n@@ @@\n-a\n+b\n",
new Diff('a', 'b')
);
}
/**
* @covers PHP_Exporter\Diff::toArray
*/
public function testComparisonErrorMessage_toArray()
{
$expected = array();
$expected[] = array('a', self::REMOVED);
$expected[] = array('b', self::ADDED);
$diff = new Diff('a', 'b');
$this->assertEquals($expected, $diff->toArray());
}
/**
* @covers PHP_Exporter\Diff::diff
*/
public function testComparisonErrorStartSame()
{
$this->assertEquals(
"--- Original\n+++ New\n@@ @@\n-ba\n+bc\n",
new Diff('ba', 'bc')
);
}
/**
* @covers PHP_Exporter\Diff::toArray
*/
public function testComparisonErrorStartSame_toArray()
{
$expected = array();
$expected[] = array('ba', self::REMOVED);
$expected[] = array('bc', self::ADDED);
$diff = new Diff('ba', 'bc');
$this->assertEquals($expected, $diff->toArray());
}
/**
* @covers PHP_Exporter\Diff::diff
*/
public function testComparisonErrorEndSame()
{
$this->assertEquals(
"--- Original\n+++ New\n@@ @@\n-ab\n+cb\n",
new Diff('ab', 'cb')
);
}
/**
* @covers PHP_Exporter\Diff::toArray
*/
public function testComparisonErrorEndSame_toArray()
{
$expected = array();
$expected[] = array('ab', self::REMOVED);
$expected[] = array('cb', self::ADDED);
$diff = new Diff('ab', 'cb');
$this->assertEquals($expected, $diff->toArray());
}
/**
* @covers PHP_Exporter\Diff::diff
*/
public function testComparisonErrorStartAndEndSame()
{
$this->assertEquals(
"--- Original\n+++ New\n@@ @@\n-abc\n+adc\n",
new Diff('abc', 'adc')
);
}
/**
* @covers PHP_Exporter\Diff::toArray
*/
public function testComparisonErrorStartAndEndSame_toArray()
{
$expected = array();
$expected[] = array('abc', self::REMOVED);
$expected[] = array('adc', self::ADDED);
$diff = new Diff('abc', 'adc');
$this->assertEquals($expected, $diff->toArray());
}
/**
* @covers PHP_Exporter\Diff::diff
*/
public function testComparisonErrorStartSameComplete()
{
$this->assertEquals(
"--- Original\n+++ New\n@@ @@\n-ab\n+abc\n",
new Diff('ab', 'abc')
);
}
/**
* @covers PHP_Exporter\Diff::toArray
*/
public function testComparisonErrorStartSameComplete_toArray()
{
$expected = array();
$expected[] = array('ab', self::REMOVED);
$expected[] = array('abc', self::ADDED);
$diff = new Diff('ab', 'abc');
$this->assertEquals($expected, $diff->toArray());
}
/**
* @covers PHP_Exporter\Diff::diff
*/
public function testComparisonErrorEndSameComplete()
{
$this->assertEquals(
"--- Original\n+++ New\n@@ @@\n-bc\n+abc\n",
new Diff('bc', 'abc')
);
}
/**
* @covers PHP_Exporter\Diff::toArray
*/
public function testComparisonErrorEndSameComplete_toArray()
{
$expected = array();
$expected[] = array('bc', self::REMOVED);
$expected[] = array('abc', self::ADDED);
$diff = new Diff('bc', 'abc');
$this->assertEquals($expected, $diff->toArray());
}
/**
* @covers PHP_Exporter\Diff::diff
*/
public function testComparisonErrorOverlapingMatches()
{
$this->assertEquals(
"--- Original\n+++ New\n@@ @@\n-abc\n+abbc\n",
new Diff('abc', 'abbc')
);
}
/**
* @covers PHP_Exporter\Diff::toArray
*/
public function testComparisonErrorOverlapingMatches_toArray()
{
$expected = array();
$expected[] = array('abc', self::REMOVED);
$expected[] = array('abbc', self::ADDED);
$diff = new Diff('abc', 'abbc');
$this->assertEquals($expected, $diff->toArray());
}
/**
* @covers PHP_Exporter\Diff::diff
*/
public function testComparisonErrorOverlapingMatches2()
{
$this->assertEquals(
"--- Original\n+++ New\n@@ @@\n-abcdde\n+abcde\n",
new Diff('abcdde', 'abcde')
);
}
/**
* @covers PHP_Exporter\Diff::toArray
*/
public function testComparisonErrorOverlapingMatches2_toArray()
{
$expected = array();
$expected[] = array('abcdde', self::REMOVED);
$expected[] = array('abcde', self::ADDED);
$diff = new Diff('abcdde', 'abcde');
$this->assertEquals($expected, $diff->toArray());
}
/**
* @covers PHP_Exporter\Diff::diff
*/
public function testEmptyDiff()
{
$this->assertEquals('', new Diff('abc', 'abc'));
}
/**
* @covers PHP_Exporter\Diff::toArray
*/
public function testEmptyDiff_toArray()
{
$diff = new Diff('abc', 'abc');
$this->assertEquals(array(), $diff->toArray());
}
public function testCustomHeader()
{
$this->assertEquals(
"CUSTOM HEADER@@ @@\n-a\n+b\n",
new Diff('a', 'b', 'CUSTOM HEADER')
);
}
}