ComparisonCompactor

In questo esempio andremo a realizzare e rifattorizzare un comparatore di stringhe. Date due stringhe differenti, come abc e abbc, mostra la differenza generando una stringa del tipo: "expected: {ab[]c} but was: {ab[b]c}".

Substring

<?php

/**
 * @param string $str
 * @param int $start
 * @param int|null $end
 * @return false|string
 */
function substring(string $str, int $start, int $end = null)
{
    if ($end !== null) {
        return substr($str, $start, $end - $start);
    }

    return substr($str, $start);
}

ComparisonCompactor (prima del refactoring)

<?php
include_once('Substring.php');

class ComparisonCompactorOriginal
{
    private const ELLIPSIS = '...';
    private const DELTA_START = '[';
    private const DELTA_END = ']';

    /** @var int */
    private $_contextLength;

    /** @var string|null */
    private $_expected;

    /** @var string|null */
    private $_actual;

    /** @var int */
    private $_prefix;

    /** @var int */
    private $_suffix;

    /**
     * ComparisonCompactorOriginal constructor.
     * @param int $contextLength
     * @param string|null $expected
     * @param string|null $actual
     */
    public function __construct(int $contextLength, ?string $expected, ?string $actual)
    {
        $this->_contextLength = $contextLength;
        $this->_expected = $expected;
        $this->_actual = $actual;
    }

    /**
     * @param string|null $message
     * @return string
     */
    public function compact(string $message = null): string
    {
        if ($this->_expected === null || $this->_actual === null || $this->areStringsEqual()) {
            return trim(sprintf('%s expected: {%s} but was: {%s}', $message, $this->_expected, $this->_actual));
        }

        $this->findCommonPrefix();
        $this->findCommonSuffix();
        $expected = $this->compactString($this->_expected);
        $actual = $this->compactString($this->_actual);
        return trim(sprintf('%s expected: {%s} but was: {%s}', $message, $expected, $actual));
    }

    /**
     * @param string $source
     * @return string
     */
    private function compactString(string $source): string
    {
        $result =
            self::DELTA_START .
            substring($source, $this->_prefix, strlen($source) - $this->_suffix + 1) .
            self::DELTA_END;

        if ($this->_prefix > 0) {
            $result = $this->computeCommonPrefix() . $result;
        }

        if ($this->_suffix > 0) {
            $result = $result . $this->computeCommonSuffix();
        }

        return $result;
    }

    /**
     * @return void
     */
    private function findCommonPrefix(): void
    {
        $this->_prefix = 0;
        $end = min(strlen($this->_expected), strlen($this->_actual));
        for (; $this->_prefix < $end; $this->_prefix++) {
            if ($this->_expected[$this->_prefix] !== $this->_actual[$this->_prefix]) {
                break;
            }
        }
    }

    /**
     * @return void
     */
    private function findCommonSuffix(): void
    {
        $expectedSuffix = strlen($this->_expected) - 1;
        $actualSuffix = strlen($this->_actual) - 1;
        for (; $actualSuffix >= $this->_prefix && $expectedSuffix >= $this->_prefix; $actualSuffix--, $expectedSuffix--) {
            if ($this->_expected[$expectedSuffix] !== $this->_actual[$actualSuffix]) {
                break;
            }
        }
        $this->_suffix = strlen($this->_expected) - $expectedSuffix;
    }

    /**
     * @return string
     */
    private function computeCommonPrefix(): string
    {
        return
            ($this->_prefix > $this->_contextLength ? self::ELLIPSIS : '') .
            (substring($this->_expected, max(0, $this->_prefix - $this->_contextLength), $this->_prefix));
    }

    /**
     * @return string
     */
    private function computeCommonSuffix(): string
    {
        $end = min(strlen($this->_expected) - $this->_suffix + 1 + $this->_contextLength, strlen($this->_expected));
        return
            (substring($this->_expected, strlen($this->_expected) - $this->_suffix + 1, $end)) .
            (strlen($this->_expected) - $this->_suffix + 1 < strlen($this->_expected) - $this->_contextLength ? self::ELLIPSIS : '');
    }

    /**
     * @return bool
     */
    private function areStringsEqual(): bool
    {
        return $this->_expected === $this->_actual;
    }
}

ComparisonCompactor (dopo il refactoring)

<?php
include_once('Substring.php');

class ComparisonCompactorFinal
{
    private const ELLIPSIS = '...';
    private const DELTA_START = '[';
    private const DELTA_END = ']';

    /** @var int */
    private $contextLength;

    /** @var string|null */
    private $expected;

    /** @var string|null */
    private $actual;

    /** @var int */
    private $prefixLength;

    /** @var int */
    private $suffixLength;

    /**
     * ComparisonCompactorFinal constructor.
     * @param int $contextLength
     * @param string|null $expected
     * @param string|null $actual
     */
    public function __construct(int $contextLength, ?string $expected, ?string $actual)
    {
        $this->contextLength = $contextLength;
        $this->expected = $expected;
        $this->actual = $actual;
    }

    /**
     * @param string|null $message
     * @return string
     */
    public function formatCompactedComparison(string $message = null): string
    {
        $compactExpected = $this->expected;
        $compactActual = $this->actual;

        if ($this->shouldBeCompacted()) {
            $this->findCommonPrefixAndSuffix();
            $compactExpected = $this->compact($this->expected);
            $compactActual = $this->compact($this->actual);
        }

        return trim(sprintf('%s expected: {%s} but was: {%s}', $message, $compactExpected, $compactActual));
    }

    /**
     * @return bool
     */
    private function shouldBeCompacted(): bool
    {
        return !$this->shouldNotBeCompacted();
    }

    /**
     * @return bool
     */
    private function shouldNotBeCompacted(): bool
    {
        return $this->expected === null || $this->actual === null || $this->expected === $this->actual;
    }

    /**
     * @return void
     */
    private function findCommonPrefixAndSuffix(): void
    {
        $this->findCommonPrefix();
        $this->suffixLength = 0;
        for (; !$this->suffixOverlapsPrefix(); $this->suffixLength++) {
            if ($this->charFromEnd($this->expected) !== $this->charFromEnd($this->actual)) {
                break;
            }
        }
    }

    /**
     * @return void
     */
    private function findCommonPrefix(): void
    {
        $this->prefixLength = 0;
        $end = min(strlen($this->expected), strlen($this->actual));
        for (; $this->prefixLength < $end; $this->prefixLength++) {
            if ($this->expected[$this->prefixLength] !== $this->actual[$this->prefixLength]) {
                break;
            }
        }
    }

    /**
     * @return bool
     */
    private function suffixOverlapsPrefix(): bool
    {
        return (
            strlen($this->actual) - $this->suffixLength <= $this->prefixLength ||
            strlen($this->expected) - $this->suffixLength <= $this->prefixLength
        );
    }

    /**
     * @param string $s
     * @return string
     */
    private function charFromEnd(string $s): string
    {
        return $s[strlen($s) - $this->suffixLength - 1];
    }

    /**
     * @param string $s
     * @return string
     */
    private function compact(string $s): string
    {
        return (
            $this->startEllipsis() .
            $this->startContext() .
            self::DELTA_START .
            $this->delta($s) .
            self::DELTA_END .
            $this->endContext() .
            $this->endEllipsis()
        );
    }

    /**
     * @return string
     */
    private function startEllipsis(): string
    {
        return $this->prefixLength > $this->contextLength ? self::ELLIPSIS : '';
    }

    /**
     * @return string
     */
    private function startContext(): string
    {
        $contextStart = max(0, $this->prefixLength - $this->contextLength);
        $contextEnd = $this->prefixLength;
        return substring($this->expected, $contextStart, $contextEnd);
    }

    /**
     * @param string $s
     * @return string
     */
    private function delta(string $s): string
    {
        $deltaStart = $this->prefixLength;
        $deltaEnd = strlen($s) - $this->suffixLength;
        return substring($s, $deltaStart, $deltaEnd);
    }

    /**
     * @return string
     */
    private function endContext(): string
    {
        $contextStart = strlen($this->expected) - $this->suffixLength;
        $contextEnd = min($contextStart + $this->contextLength, strlen($this->expected));
        return substring($this->expected, $contextStart, $contextEnd);
    }

    /**
     * @return string
     */
    private function endEllipsis(): string
    {
        return $this->suffixLength > $this->contextLength ? self::ELLIPSIS : '';
    }
}

Index

<?php
include_once('ComparisonCompactorOriginal.php');
include_once('ComparisonCompactorFinal.php');

$contextLength = 2;
$expected = 'abc';
$actual = 'abbc';

$comparisonOriginal = new ComparisonCompactorOriginal($contextLength, $expected, $actual);
print_r(nl2br('<b>Original: </b>' . PHP_EOL . $comparisonOriginal->compact()));

print_r(nl2br(PHP_EOL . PHP_EOL));

$comparisonFinal = new ComparisonCompactorFinal($contextLength, $expected, $actual);
print_r(nl2br('<b> Final: </b>' . PHP_EOL . $comparisonFinal->formatCompactedComparison()));

Last updated