<?php
/*
- * This file is part of the Diff package.
+ * This file is part of sebastian/diff.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
/**
* @param string $header
+ * @param bool $showNonDiffLines
*/
public function __construct($header = "--- Original\n+++ New\n", $showNonDiffLines = true)
{
*/
public function diff($from, $to, LongestCommonSubsequence $lcs = null)
{
- if (!is_array($from) && !is_string($from)) {
- $from = (string) $from;
+ $from = $this->validateDiffInput($from);
+ $to = $this->validateDiffInput($to);
+ $diff = $this->diffToArray($from, $to, $lcs);
+ $old = $this->checkIfDiffInOld($diff);
+ $start = isset($old[0]) ? $old[0] : 0;
+ $end = \count($diff);
+
+ if ($tmp = \array_search($end, $old)) {
+ $end = $tmp;
}
- if (!is_array($to) && !is_string($to)) {
- $to = (string) $to;
+ return $this->getBuffer($diff, $old, $start, $end);
+ }
+
+ /**
+ * Casts variable to string if it is not a string or array.
+ *
+ * @param mixed $input
+ *
+ * @return string
+ */
+ private function validateDiffInput($input)
+ {
+ if (!\is_array($input) && !\is_string($input)) {
+ return (string) $input;
}
- $buffer = $this->header;
- $diff = $this->diffToArray($from, $to, $lcs);
+ return $input;
+ }
+ /**
+ * Takes input of the diff array and returns the old array.
+ * Iterates through diff line by line,
+ *
+ * @param array $diff
+ *
+ * @return array
+ */
+ private function checkIfDiffInOld(array $diff)
+ {
$inOld = false;
$i = 0;
$old = array();
foreach ($diff as $line) {
- if ($line[1] === 0 /* OLD */) {
+ if ($line[1] === 0 /* OLD */) {
if ($inOld === false) {
$inOld = $i;
}
++$i;
}
- $start = isset($old[0]) ? $old[0] : 0;
- $end = count($diff);
+ return $old;
+ }
- if ($tmp = array_search($end, $old)) {
- $end = $tmp;
- }
+ /**
+ * Generates buffer in string format, returning the patch.
+ *
+ * @param array $diff
+ * @param array $old
+ * @param int $start
+ * @param int $end
+ *
+ * @return string
+ */
+ private function getBuffer(array $diff, array $old, $start, $end)
+ {
+ $buffer = $this->header;
- $newChunk = true;
+ if (!isset($old[$start])) {
+ $buffer = $this->getDiffBufferElementNew($diff, $buffer, $start);
+ ++$start;
+ }
for ($i = $start; $i < $end; $i++) {
if (isset($old[$i])) {
- $buffer .= "\n";
- $newChunk = true;
- $i = $old[$i];
+ $i = $old[$i];
+ $buffer = $this->getDiffBufferElementNew($diff, $buffer, $i);
+ } else {
+ $buffer = $this->getDiffBufferElement($diff, $buffer, $i);
}
+ }
- if ($newChunk) {
- if ($this->showNonDiffLines === true) {
- $buffer .= "@@ @@\n";
- }
- $newChunk = false;
- }
+ return $buffer;
+ }
- if ($diff[$i][1] === 1 /* ADDED */) {
- $buffer .= '+' . $diff[$i][0] . "\n";
- } elseif ($diff[$i][1] === 2 /* REMOVED */) {
- $buffer .= '-' . $diff[$i][0] . "\n";
- } elseif ($this->showNonDiffLines === true) {
- $buffer .= ' ' . $diff[$i][0] . "\n";
- }
+ /**
+ * Gets individual buffer element.
+ *
+ * @param array $diff
+ * @param string $buffer
+ * @param int $diffIndex
+ *
+ * @return string
+ */
+ private function getDiffBufferElement(array $diff, $buffer, $diffIndex)
+ {
+ if ($diff[$diffIndex][1] === 1 /* ADDED */) {
+ $buffer .= '+' . $diff[$diffIndex][0] . "\n";
+ } elseif ($diff[$diffIndex][1] === 2 /* REMOVED */) {
+ $buffer .= '-' . $diff[$diffIndex][0] . "\n";
+ } elseif ($this->showNonDiffLines === true) {
+ $buffer .= ' ' . $diff[$diffIndex][0] . "\n";
}
return $buffer;
}
+ /**
+ * Gets individual buffer element with opening.
+ *
+ * @param array $diff
+ * @param string $buffer
+ * @param int $diffIndex
+ *
+ * @return string
+ */
+ private function getDiffBufferElementNew(array $diff, $buffer, $diffIndex)
+ {
+ if ($this->showNonDiffLines === true) {
+ $buffer .= "@@ @@\n";
+ }
+
+ return $this->getDiffBufferElement($diff, $buffer, $diffIndex);
+ }
+
/**
* Returns the diff between two arrays or strings as array.
*
* Each array element contains two elements:
- * - [0] => string $token
+ * - [0] => mixed $token
* - [1] => 2|1|0
*
* - 2: REMOVED: $token was removed from $from
*/
public function diffToArray($from, $to, LongestCommonSubsequence $lcs = null)
{
- preg_match_all('(\r\n|\r|\n)', $from, $fromMatches);
- preg_match_all('(\r\n|\r|\n)', $to, $toMatches);
-
- if (is_string($from)) {
- $from = preg_split('(\r\n|\r|\n)', $from);
+ if (\is_string($from)) {
+ $fromMatches = $this->getNewLineMatches($from);
+ $from = $this->splitStringByLines($from);
+ } elseif (\is_array($from)) {
+ $fromMatches = array();
+ } else {
+ throw new \InvalidArgumentException('"from" must be an array or string.');
}
- if (is_string($to)) {
- $to = preg_split('(\r\n|\r|\n)', $to);
+ if (\is_string($to)) {
+ $toMatches = $this->getNewLineMatches($to);
+ $to = $this->splitStringByLines($to);
+ } elseif (\is_array($to)) {
+ $toMatches = array();
+ } else {
+ throw new \InvalidArgumentException('"to" must be an array or string.');
}
- $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;
- }
- }
+ list($from, $to, $start, $end) = self::getArrayDiffParted($from, $to);
if ($lcs === null) {
$lcs = $this->selectLcsImplementation($from, $to);
}
- $common = $lcs->calculate(array_values($from), array_values($to));
+ $common = $lcs->calculate(\array_values($from), \array_values($to));
$diff = array();
- if (isset($fromMatches[0]) && $toMatches[0] &&
- count($fromMatches[0]) === count($toMatches[0]) &&
- $fromMatches[0] !== $toMatches[0]) {
+ if ($this->detectUnmatchedLineEndings($fromMatches, $toMatches)) {
$diff[] = array(
- '#Warning: Strings contain different line endings!', 0
+ '#Warning: Strings contain different line endings!',
+ 0
);
}
$diff[] = array($token, 0 /* OLD */);
}
- reset($from);
- reset($to);
+ \reset($from);
+ \reset($to);
foreach ($common as $token) {
- while ((($fromToken = reset($from)) !== $token)) {
- $diff[] = array(array_shift($from), 2 /* REMOVED */);
+ while (($fromToken = \reset($from)) !== $token) {
+ $diff[] = array(\array_shift($from), 2 /* REMOVED */);
}
- while ((($toToken = reset($to)) !== $token)) {
- $diff[] = array(array_shift($to), 1 /* ADDED */);
+ while (($toToken = \reset($to)) !== $token) {
+ $diff[] = array(\array_shift($to), 1 /* ADDED */);
}
$diff[] = array($token, 0 /* OLD */);
- array_shift($from);
- array_shift($to);
+ \array_shift($from);
+ \array_shift($to);
}
- while (($token = array_shift($from)) !== null) {
+ while (($token = \array_shift($from)) !== null) {
$diff[] = array($token, 2 /* REMOVED */);
}
- while (($token = array_shift($to)) !== null) {
+ while (($token = \array_shift($to)) !== null) {
$diff[] = array($token, 1 /* ADDED */);
}
return $diff;
}
+ /**
+ * Get new strings denoting new lines from a given string.
+ *
+ * @param string $string
+ *
+ * @return array
+ */
+ private function getNewLineMatches($string)
+ {
+ \preg_match_all('(\r\n|\r|\n)', $string, $stringMatches);
+
+ return $stringMatches;
+ }
+
+ /**
+ * Checks if input is string, if so it will split it line-by-line.
+ *
+ * @param string $input
+ *
+ * @return array
+ */
+ private function splitStringByLines($input)
+ {
+ return \preg_split('(\r\n|\r|\n)', $input);
+ }
+
/**
* @param array $from
* @param array $to
* @param array $from
* @param array $to
*
- * @return int
+ * @return int|float
*/
private function calculateEstimatedFootprint(array $from, array $to)
{
- $itemSize = PHP_INT_SIZE == 4 ? 76 : 144;
+ $itemSize = PHP_INT_SIZE === 4 ? 76 : 144;
+
+ return $itemSize * \pow(\min(\count($from), \count($to)), 2);
+ }
+
+ /**
+ * Returns true if line ends don't match on fromMatches and toMatches.
+ *
+ * @param array $fromMatches
+ * @param array $toMatches
+ *
+ * @return bool
+ */
+ private function detectUnmatchedLineEndings(array $fromMatches, array $toMatches)
+ {
+ return isset($fromMatches[0], $toMatches[0]) &&
+ \count($fromMatches[0]) === \count($toMatches[0]) &&
+ $fromMatches[0] !== $toMatches[0];
+ }
+
+ /**
+ * @param array $from
+ * @param array $to
+ *
+ * @return array
+ */
+ private static function getArrayDiffParted(array &$from, array &$to)
+ {
+ $start = array();
+ $end = array();
+
+ \reset($to);
+
+ foreach ($from as $k => $v) {
+ $toK = \key($to);
+
+ if ($toK === $k && $v === $to[$k]) {
+ $start[$k] = $v;
+
+ unset($from[$k], $to[$k]);
+ } else {
+ break;
+ }
+ }
+
+ \end($from);
+ \end($to);
+
+ do {
+ $fromK = \key($from);
+ $toK = \key($to);
+
+ if (null === $fromK || null === $toK || \current($from) !== \current($to)) {
+ break;
+ }
+
+ \prev($from);
+ \prev($to);
+
+ $end = array($fromK => $from[$fromK]) + $end;
+ unset($from[$fromK], $to[$toK]);
+ } while (true);
- return $itemSize * pow(min(count($from), count($to)), 2);
+ return array($from, $to, $start, $end);
}
}