| | |
| | class GitDiff { |
| | - private Git $git; |
| | - private const MAX_DIFF_SIZE = 262144; |
| | - |
| | - public function __construct( Git $git ) { |
| | - $this->git = $git; |
| | - } |
| | - |
| | - public function diff( string $oldSha, string $newSha ): Generator { |
| | - $oldTree = $oldSha !== '' ? $this->getTreeHash( $oldSha ) : ''; |
| | - $newTree = $newSha !== '' ? $this->getTreeHash( $newSha ) : ''; |
| | - |
| | - yield from $this->diffTrees( $oldTree, $newTree ); |
| | - } |
| | - |
| | - public function compare( string $commitHash ): Generator { |
| | - $commitData = $this->git->read( $commitHash ); |
| | - $parentHash = ''; |
| | - |
| | - if( preg_match( '/^parent ([0-9a-f]{40})/m', $commitData, $m ) ) { |
| | - $parentHash = $m[1]; |
| | - } |
| | - |
| | - $newTree = $this->getTreeHash( $commitHash ); |
| | - $oldTree = $parentHash !== '' ? $this->getTreeHash( $parentHash ) : ''; |
| | - |
| | - yield from $this->diffTrees( $oldTree, $newTree ); |
| | - } |
| | - |
| | - private function getTreeHash( string $commitSha ): string { |
| | - $data = $this->git->read( $commitSha ); |
| | - $result = ''; |
| | - |
| | - if( preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) { |
| | - $result = $matches[1]; |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - private function diffTrees( |
| | - string $oldTreeSha, |
| | - string $newTreeSha, |
| | - string $path = '' |
| | - ): Generator { |
| | - if( $oldTreeSha !== $newTreeSha ) { |
| | - $oldEntries = $oldTreeSha !== '' |
| | - ? $this->parseTree( $oldTreeSha ) |
| | - : []; |
| | - $newEntries = $newTreeSha !== '' |
| | - ? $this->parseTree( $newTreeSha ) |
| | - : []; |
| | - $allNames = array_unique( |
| | - array_merge( array_keys( $oldEntries ), array_keys( $newEntries ) ) |
| | - ); |
| | - |
| | - sort( $allNames ); |
| | - |
| | - foreach( $allNames as $name ) { |
| | - $old = $oldEntries[$name] ?? null; |
| | - $new = $newEntries[$name] ?? null; |
| | - $currentPath = $path !== '' ? "$path/$name" : $name; |
| | - |
| | - if( !$old && $new ) { |
| | - if( $new['file']->isDir() ) { |
| | - yield from $this->diffTrees( '', $new['sha'], $currentPath ); |
| | - } else { |
| | - yield $this->createChange( |
| | - 'A', |
| | - $currentPath, |
| | - '', |
| | - $new['sha'], |
| | - null, |
| | - $new['file'] |
| | - ); |
| | - } |
| | - } elseif( !$new && $old ) { |
| | - if( $old['file']->isDir() ) { |
| | - yield from $this->diffTrees( $old['sha'], '', $currentPath ); |
| | - } else { |
| | - yield $this->createChange( |
| | - 'D', |
| | - $currentPath, |
| | - $old['sha'], |
| | - '', |
| | - $old['file'], |
| | - null |
| | - ); |
| | - } |
| | - } elseif( $old && $new && $old['sha'] !== $new['sha'] ) { |
| | - if( $old['file']->isDir() && $new['file']->isDir() ) { |
| | - yield from $this->diffTrees( |
| | - $old['sha'], |
| | - $new['sha'], |
| | - $currentPath |
| | - ); |
| | - } elseif( !$old['file']->isDir() && !$new['file']->isDir() ) { |
| | - yield $this->createChange( |
| | - 'M', |
| | - $currentPath, |
| | - $old['sha'], |
| | - $new['sha'], |
| | - $old['file'], |
| | - $new['file'] |
| | - ); |
| | - } |
| | - } |
| | - } |
| | - } |
| | - } |
| | - |
| | - private function parseTree( string $sha ): array { |
| | - $data = $this->git->read( $sha ); |
| | - $entries = []; |
| | - |
| | - $this->git->parseTreeData( |
| | - $data, |
| | - function( $file, $name, $hash ) use ( &$entries ) { |
| | - $entries[$name] = [ |
| | - 'file' => $file, |
| | - 'sha' => $hash |
| | - ]; |
| | - } |
| | - ); |
| | - |
| | - return $entries; |
| | - } |
| | - |
| | - private function createChange( |
| | - string $type, |
| | - string $path, |
| | - string $oldSha, |
| | - string $newSha, |
| | - ?File $oldFile = null, |
| | - ?File $newFile = null |
| | - ): array { |
| | - $oldSize = $oldSha !== '' ? $this->git->getObjectSize( $oldSha ) : 0; |
| | - $newSize = $newSha !== '' ? $this->git->getObjectSize( $newSha ) : 0; |
| | - $result = []; |
| | - |
| | - if( $oldSize > self::MAX_DIFF_SIZE || $newSize > self::MAX_DIFF_SIZE ) { |
| | - $result = [ |
| | - 'type' => $type, |
| | - 'path' => $path, |
| | - 'is_binary' => true, |
| | - 'hunks' => [] |
| | - ]; |
| | - } else { |
| | - $oldContent = $oldSha !== '' ? $this->git->read( $oldSha ) : ''; |
| | - $newContent = $newSha !== '' ? $this->git->read( $newSha ) : ''; |
| | - $isBinary = false; |
| | - |
| | - if( $newFile !== null ) { |
| | - $isBinary = $newFile->isBinary(); |
| | - } elseif( $oldFile !== null ) { |
| | - $isBinary = $oldFile->isBinary(); |
| | - } |
| | - |
| | - $result = [ |
| | - 'type' => $type, |
| | - 'path' => $path, |
| | - 'is_binary' => $isBinary, |
| | - 'hunks' => $isBinary |
| | - ? null |
| | - : $this->calculateDiff( $oldContent, $newContent ) |
| | - ]; |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - private function calculateDiff( string $old, string $new ): array { |
| | - $oldLines = explode( "\n", str_replace( "\r\n", "\n", $old ) ); |
| | - $newLines = explode( "\n", str_replace( "\r\n", "\n", $new ) ); |
| | - $m = count( $oldLines ); |
| | - $n = count( $newLines ); |
| | - $start = 0; |
| | - $end = 0; |
| | - |
| | - while( $start < $m && $start < $n && |
| | - $oldLines[$start] === $newLines[$start] ) { |
| | - $start++; |
| | - } |
| | - |
| | - while( $m - $end > $start && $n - $end > $start && |
| | - $oldLines[$m - 1 - $end] === $newLines[$n - 1 - $end] ) { |
| | - $end++; |
| | - } |
| | - |
| | - $stream = $this->buildFullStream( |
| | - $oldLines, |
| | - $newLines, |
| | - $m, |
| | - $n, |
| | - $start, |
| | - $end |
| | - ); |
| | - |
| | - return $this->formatDiffOutput( $stream ); |
| | - } |
| | - |
| | - private function buildFullStream( |
| | - array $oldLines, |
| | - array $newLines, |
| | - int $m, |
| | - int $n, |
| | - int $start, |
| | - int $end |
| | - ): array { |
| | - $context = 2; |
| | - $limit = 100000; |
| | - $stream = []; |
| | - $pStart = max( 0, $start - $context ); |
| | - |
| | - for( $i = $pStart; $i < $start; $i++ ) { |
| | - $stream[] = [ |
| | - 't' => ' ', |
| | - 'l' => $oldLines[$i], |
| | - 'no' => $i + 1, |
| | - 'nn' => $i + 1 |
| | - ]; |
| | - } |
| | - |
| | - $oldSlice = array_slice( $oldLines, $start, $m - $start - $end ); |
| | - $newSlice = array_slice( $newLines, $start, $n - $start - $end ); |
| | - $mid = (count( $oldSlice ) * count( $newSlice )) > $limit |
| | - ? $this->buildFallbackDiff( $oldSlice, $newSlice, $start ) |
| | - : $this->buildDiffStream( |
| | - $this->computeLCS( $oldSlice, $newSlice ), |
| | - $start |
| | - ); |
| | - |
| | - foreach( $mid as $line ) { |
| | - $stream[] = $line; |
| | - } |
| | - |
| | - $sLimit = min( $end, $context ); |
| | - |
| | - for( $i = 0; $i < $sLimit; $i++ ) { |
| | - $idxO = $m - $end + $i; |
| | - $idxN = $n - $end + $i; |
| | - |
| | - $stream[] = [ |
| | - 't' => ' ', |
| | - 'l' => $oldLines[$idxO], |
| | - 'no' => $idxO + 1, |
| | - 'nn' => $idxN + 1 |
| | - ]; |
| | - } |
| | - |
| | - return $stream; |
| | - } |
| | - |
| | - private function formatDiffOutput( array $stream ): array { |
| | - $n = count( $stream ); |
| | - $keep = array_fill( 0, $n, false ); |
| | - $context = 2; |
| | - |
| | - for( $i = 0; $i < $n; $i++ ) { |
| | - if( $stream[$i]['t'] !== ' ' ) { |
| | - $low = max( 0, $i - $context ); |
| | - $high = min( $n - 1, $i + $context ); |
| | - |
| | - for( $j = $low; $j <= $high; $j++ ) { |
| | - $keep[$j] = true; |
| | - } |
| | - } |
| | - } |
| | - |
| | - $result = []; |
| | - $buffer = []; |
| | - |
| | - for( $i = 0; $i < $n; $i++ ) { |
| | - if( $keep[$i] ) { |
| | - $this->flushBuffer( $result, $buffer ); |
| | - |
| | - $result[] = $stream[$i]; |
| | - } else { |
| | - $buffer[] = $stream[$i]; |
| | - } |
| | - } |
| | - |
| | - $this->flushBuffer( $result, $buffer ); |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - private function flushBuffer( array &$result, array &$buffer ): void { |
| | - $cnt = count( $buffer ); |
| | - |
| | - if( $cnt > 0 ) { |
| | - if( $cnt > 5 ) { |
| | - $result[] = [ 't' => 'gap' ]; |
| | - } else { |
| | - foreach( $buffer as $bufLine ) { |
| | - $result[] = $bufLine; |
| | - } |
| | - } |
| | - |
| | - $buffer = []; |
| | - } |
| | - } |
| | - |
| | - private function buildFallbackDiff( |
| | - array $old, |
| | - array $new, |
| | - int $offset |
| | - ): array { |
| | - $stream = []; |
| | - $currO = $offset + 1; |
| | - $currN = $offset + 1; |
| | - |
| | - foreach( $old as $line ) { |
| | - $stream[] = [ |
| | - 't' => '-', |
| | - 'l' => $line, |
| | - 'no' => $currO++, |
| | - 'nn' => null |
| | - ]; |
| | - } |
| | - |
| | - foreach( $new as $line ) { |
| | - $stream[] = [ |
| | - 't' => '+', |
| | - 'l' => $line, |
| | - 'no' => null, |
| | - 'nn' => $currN++ |
| | - ]; |
| | - } |
| | - |
| | - return $stream; |
| | - } |
| | - |
| | - private function buildDiffStream( array $ops, int $start ): array { |
| | - $stream = []; |
| | - $currO = $start + 1; |
| | - $currN = $start + 1; |
| | - |
| | - foreach( $ops as $op ) { |
| | - $stream[] = [ |
| | - 't' => $op['t'], |
| | - 'l' => $op['l'], |
| | - 'no' => $op['t'] === '+' ? null : $currO++, |
| | - 'nn' => $op['t'] === '-' ? null : $currN++ |
| | - ]; |
| | - } |
| | - |
| | - return $stream; |
| | - } |
| | - |
| | - private function computeLCS( array $old, array $new ): array { |
| | - $m = count( $old ); |
| | - $n = count( $new ); |
| | - $c = array_fill( 0, $m + 1, array_fill( 0, $n + 1, 0 ) ); |
| | - |
| | - for( $i = 1; $i <= $m; $i++ ) { |
| | - for( $j = 1; $j <= $n; $j++ ) { |
| | - $c[$i][$j] = ($old[$i - 1] === $new[$j - 1]) |
| | + private const MAX_DIFF_SIZE = 262144; |
| | + |
| | + private Git $git; |
| | + |
| | + public function __construct( Git $git ) { |
| | + $this->git = $git; |
| | + } |
| | + |
| | + public function diff( string $oldSha, string $newSha ): Generator { |
| | + $oldTree = $oldSha === '' ? '' : $this->getTreeHash( $oldSha ); |
| | + $newTree = $newSha === '' ? '' : $this->getTreeHash( $newSha ); |
| | + |
| | + yield from $this->diffTrees( $oldTree, $newTree ); |
| | + } |
| | + |
| | + public function compare( string $commitHash ): Generator { |
| | + $commitData = $this->git->read( $commitHash ); |
| | + $parentHash = ''; |
| | + |
| | + if( preg_match( '/^parent ([0-9a-f]{40})/m', $commitData, $m ) ) { |
| | + $parentHash = $m[1]; |
| | + } |
| | + |
| | + $newTree = $this->getTreeHash( $commitHash ); |
| | + $oldTree = $parentHash === '' |
| | + ? '' |
| | + : $this->getTreeHash( $parentHash ); |
| | + |
| | + yield from $this->diffTrees( $oldTree, $newTree ); |
| | + } |
| | + |
| | + private function getTreeHash( string $commitSha ): string { |
| | + $data = $this->git->read( $commitSha ); |
| | + $result = ''; |
| | + |
| | + if( preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) { |
| | + $result = $matches[1]; |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + private function diffTrees( |
| | + string $oldTreeSha, |
| | + string $newTreeSha, |
| | + string $path = '' |
| | + ): Generator { |
| | + if( $oldTreeSha !== $newTreeSha ) { |
| | + $oldEntries = $oldTreeSha === '' |
| | + ? [] |
| | + : $this->parseTree( $oldTreeSha ); |
| | + $newEntries = $newTreeSha === '' |
| | + ? [] |
| | + : $this->parseTree( $newTreeSha ); |
| | + $allNames = array_unique( |
| | + array_merge( |
| | + array_keys( $oldEntries ), |
| | + array_keys( $newEntries ) |
| | + ) |
| | + ); |
| | + |
| | + sort( $allNames ); |
| | + |
| | + static $emptyFile = new File( '', '', '0', 0, 0 ); |
| | + |
| | + foreach( $allNames as $name ) { |
| | + $old = $oldEntries[$name] ?? []; |
| | + $new = $newEntries[$name] ?? []; |
| | + $hasOld = !empty( $old ); |
| | + $hasNew = !empty( $new ); |
| | + $currentPath = $path === '' ? $name : "$path/$name"; |
| | + $oldFile = $hasOld ? $old['file'] : $emptyFile; |
| | + $newFile = $hasNew ? $new['file'] : $emptyFile; |
| | + $oldSha = $hasOld ? $old['sha'] : ''; |
| | + $newSha = $hasNew ? $new['sha'] : ''; |
| | + |
| | + if( !$hasOld && $hasNew ) { |
| | + if( $newFile->isDir() ) { |
| | + yield from $this->diffTrees( '', $newSha, $currentPath ); |
| | + } else { |
| | + yield $this->createChange( |
| | + 'A', |
| | + $currentPath, |
| | + '', |
| | + $newSha, |
| | + $oldFile, |
| | + $newFile |
| | + ); |
| | + } |
| | + } elseif( !$hasNew && $hasOld ) { |
| | + if( $oldFile->isDir() ) { |
| | + yield from $this->diffTrees( $oldSha, '', $currentPath ); |
| | + } else { |
| | + yield $this->createChange( |
| | + 'D', |
| | + $currentPath, |
| | + $oldSha, |
| | + '', |
| | + $oldFile, |
| | + $newFile |
| | + ); |
| | + } |
| | + } elseif( $hasOld && $hasNew && $oldSha !== $newSha ) { |
| | + if( $oldFile->isDir() && $newFile->isDir() ) { |
| | + yield from $this->diffTrees( |
| | + $oldSha, |
| | + $newSha, |
| | + $currentPath |
| | + ); |
| | + } elseif( !$oldFile->isDir() && !$newFile->isDir() ) { |
| | + yield $this->createChange( |
| | + 'M', |
| | + $currentPath, |
| | + $oldSha, |
| | + $newSha, |
| | + $oldFile, |
| | + $newFile |
| | + ); |
| | + } |
| | + } |
| | + } |
| | + } |
| | + } |
| | + |
| | + private function parseTree( string $sha ): array { |
| | + $data = $this->git->read( $sha ); |
| | + $entries = []; |
| | + |
| | + $this->git->parseTreeData( |
| | + $data, |
| | + function( string $name, string $hash, string $mode ) use ( |
| | + &$entries |
| | + ) { |
| | + $dir = $mode === '40000' || $mode === '040000'; |
| | + $isSub = $mode === '160000'; |
| | + |
| | + $entries[$name] = [ |
| | + 'file' => new File( |
| | + $name, |
| | + $hash, |
| | + $mode, |
| | + 0, |
| | + $dir || $isSub ? 0 : $this->git->getObjectSize( $hash ), |
| | + $dir || $isSub ? '' : $this->git->peek( $hash ) |
| | + ), |
| | + 'sha' => $hash |
| | + ]; |
| | + } |
| | + ); |
| | + |
| | + return $entries; |
| | + } |
| | + |
| | + private function createChange( |
| | + string $type, |
| | + string $path, |
| | + string $oldSha, |
| | + string $newSha, |
| | + File $oldFile, |
| | + File $newFile |
| | + ): array { |
| | + $oldSize = $oldSha === '' |
| | + ? 0 |
| | + : $this->git->getObjectSize( $oldSha ); |
| | + $newSize = $newSha === '' |
| | + ? 0 |
| | + : $this->git->getObjectSize( $newSha ); |
| | + $result = []; |
| | + |
| | + if( $oldSize > self::MAX_DIFF_SIZE |
| | + || $newSize > self::MAX_DIFF_SIZE |
| | + ) { |
| | + $result = [ |
| | + 'type' => $type, |
| | + 'path' => $path, |
| | + 'is_binary' => true, |
| | + 'hunks' => [] |
| | + ]; |
| | + } else { |
| | + $oldContent = $oldSha === '' ? '' : $this->git->read( $oldSha ); |
| | + $newContent = $newSha === '' ? '' : $this->git->read( $newSha ); |
| | + $isBinary = $oldFile->isBinary() || $newFile->isBinary(); |
| | + |
| | + $result = [ |
| | + 'type' => $type, |
| | + 'path' => $path, |
| | + 'is_binary' => $isBinary, |
| | + 'hunks' => $isBinary |
| | + ? [] |
| | + : $this->calculateDiff( $oldContent, $newContent ) |
| | + ]; |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + private function calculateDiff( string $old, string $new ): array { |
| | + $oldLines = explode( "\n", str_replace( "\r\n", "\n", $old ) ); |
| | + $newLines = explode( "\n", str_replace( "\r\n", "\n", $new ) ); |
| | + $m = count( $oldLines ); |
| | + $n = count( $newLines ); |
| | + $start = 0; |
| | + $end = 0; |
| | + |
| | + while( $start < $m && $start < $n && |
| | + $oldLines[$start] === $newLines[$start] ) { |
| | + $start++; |
| | + } |
| | + |
| | + while( $m - $end > $start && $n - $end > $start && |
| | + $oldLines[$m - 1 - $end] === $newLines[$n - 1 - $end] ) { |
| | + $end++; |
| | + } |
| | + |
| | + $stream = $this->buildFullStream( |
| | + $oldLines, |
| | + $newLines, |
| | + $m, |
| | + $n, |
| | + $start, |
| | + $end |
| | + ); |
| | + |
| | + return $this->formatDiffOutput( $stream ); |
| | + } |
| | + |
| | + private function buildFullStream( |
| | + array $oldLines, |
| | + array $newLines, |
| | + int $m, |
| | + int $n, |
| | + int $start, |
| | + int $end |
| | + ): array { |
| | + $context = 2; |
| | + $limit = 100000; |
| | + $stream = []; |
| | + $pStart = max( 0, $start - $context ); |
| | + |
| | + for( $i = $pStart; $i < $start; $i++ ) { |
| | + $stream[] = [ |
| | + 't' => ' ', |
| | + 'l' => $oldLines[$i], |
| | + 'no' => $i + 1, |
| | + 'nn' => $i + 1 |
| | + ]; |
| | + } |
| | + |
| | + $oldSlice = array_slice( $oldLines, $start, $m - $start - $end ); |
| | + $newSlice = array_slice( $newLines, $start, $n - $start - $end ); |
| | + $mid = count( $oldSlice ) * count( $newSlice ) > $limit |
| | + ? $this->buildFallbackDiff( $oldSlice, $newSlice, $start ) |
| | + : $this->buildDiffStream( |
| | + $this->computeLCS( $oldSlice, $newSlice ), |
| | + $start |
| | + ); |
| | + |
| | + foreach( $mid as $line ) { |
| | + $stream[] = $line; |
| | + } |
| | + |
| | + $sLimit = min( $end, $context ); |
| | + |
| | + for( $i = 0; $i < $sLimit; $i++ ) { |
| | + $idxO = $m - $end + $i; |
| | + $idxN = $n - $end + $i; |
| | + |
| | + $stream[] = [ |
| | + 't' => ' ', |
| | + 'l' => $oldLines[$idxO], |
| | + 'no' => $idxO + 1, |
| | + 'nn' => $idxN + 1 |
| | + ]; |
| | + } |
| | + |
| | + return $stream; |
| | + } |
| | + |
| | + private function formatDiffOutput( array $stream ): array { |
| | + $n = count( $stream ); |
| | + $keep = array_fill( 0, $n, false ); |
| | + $context = 2; |
| | + |
| | + for( $i = 0; $i < $n; $i++ ) { |
| | + if( $stream[$i]['t'] !== ' ' ) { |
| | + $low = max( 0, $i - $context ); |
| | + $high = min( $n - 1, $i + $context ); |
| | + |
| | + for( $j = $low; $j <= $high; $j++ ) { |
| | + $keep[$j] = true; |
| | + } |
| | + } |
| | + } |
| | + |
| | + $result = []; |
| | + $buffer = []; |
| | + |
| | + for( $i = 0; $i < $n; $i++ ) { |
| | + if( $keep[$i] ) { |
| | + $this->flushBuffer( $result, $buffer ); |
| | + |
| | + $result[] = $stream[$i]; |
| | + } else { |
| | + $buffer[] = $stream[$i]; |
| | + } |
| | + } |
| | + |
| | + $this->flushBuffer( $result, $buffer ); |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + private function flushBuffer( array &$result, array &$buffer ): void { |
| | + $cnt = count( $buffer ); |
| | + |
| | + if( $cnt > 0 ) { |
| | + if( $cnt > 5 ) { |
| | + $result[] = [ 't' => 'gap' ]; |
| | + } else { |
| | + foreach( $buffer as $bufLine ) { |
| | + $result[] = $bufLine; |
| | + } |
| | + } |
| | + |
| | + $buffer = []; |
| | + } |
| | + } |
| | + |
| | + private function buildFallbackDiff( |
| | + array $old, |
| | + array $new, |
| | + int $offset |
| | + ): array { |
| | + $stream = []; |
| | + $currO = $offset + 1; |
| | + $currN = $offset + 1; |
| | + |
| | + foreach( $old as $line ) { |
| | + $stream[] = [ |
| | + 't' => '-', |
| | + 'l' => $line, |
| | + 'no' => $currO++, |
| | + 'nn' => null |
| | + ]; |
| | + } |
| | + |
| | + foreach( $new as $line ) { |
| | + $stream[] = [ |
| | + 't' => '+', |
| | + 'l' => $line, |
| | + 'no' => null, |
| | + 'nn' => $currN++ |
| | + ]; |
| | + } |
| | + |
| | + return $stream; |
| | + } |
| | + |
| | + private function buildDiffStream( array $ops, int $start ): array { |
| | + $stream = []; |
| | + $currO = $start + 1; |
| | + $currN = $start + 1; |
| | + |
| | + foreach( $ops as $op ) { |
| | + $stream[] = [ |
| | + 't' => $op['t'], |
| | + 'l' => $op['l'], |
| | + 'no' => $op['t'] === '+' ? null : $currO++, |
| | + 'nn' => $op['t'] === '-' ? null : $currN++ |
| | + ]; |
| | + } |
| | + |
| | + return $stream; |
| | + } |
| | + |
| | + private function computeLCS( array $old, array $new ): array { |
| | + $m = count( $old ); |
| | + $n = count( $new ); |
| | + $c = array_fill( 0, $m + 1, array_fill( 0, $n + 1, 0 ) ); |
| | + |
| | + for( $i = 1; $i <= $m; $i++ ) { |
| | + for( $j = 1; $j <= $n; $j++ ) { |
| | + $c[$i][$j] = $old[$i - 1] === $new[$j - 1] |
| | ? $c[$i - 1][$j - 1] + 1 |
| | : max( $c[$i][$j - 1], $c[$i - 1][$j] ); |