ScalarComparator.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of sebastian/comparator.
  4. *
  5. * (c) Sebastian Bergmann <sebastian@phpunit.de>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace SebastianBergmann\Comparator;
  11. use function is_bool;
  12. use function is_object;
  13. use function is_scalar;
  14. use function is_string;
  15. use function mb_strtolower;
  16. use function method_exists;
  17. use function sprintf;
  18. use function strlen;
  19. use function substr;
  20. use SebastianBergmann\Exporter\Exporter;
  21. /**
  22. * Compares scalar or NULL values for equality.
  23. */
  24. class ScalarComparator extends Comparator
  25. {
  26. private const OVERLONG_THRESHOLD = 40;
  27. private const KEEP_CONTEXT_CHARS = 25;
  28. public function accepts(mixed $expected, mixed $actual): bool
  29. {
  30. return ((is_scalar($expected) xor null === $expected) &&
  31. (is_scalar($actual) xor null === $actual)) ||
  32. // allow comparison between strings and objects featuring __toString()
  33. (is_string($expected) && is_object($actual) && method_exists($actual, '__toString')) ||
  34. (is_object($expected) && method_exists($expected, '__toString') && is_string($actual));
  35. }
  36. /**
  37. * @throws ComparisonFailure
  38. */
  39. public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false): void
  40. {
  41. $expectedToCompare = $expected;
  42. $actualToCompare = $actual;
  43. $exporter = new Exporter;
  44. // always compare as strings to avoid strange behaviour
  45. // otherwise 0 == 'Foobar'
  46. if ((is_string($expected) && !is_bool($actual)) || (is_string($actual) && !is_bool($expected))) {
  47. /** @phpstan-ignore cast.string */
  48. $expectedToCompare = @(string) $expectedToCompare;
  49. /** @phpstan-ignore cast.string */
  50. $actualToCompare = @(string) $actualToCompare;
  51. if ($ignoreCase) {
  52. $expectedToCompare = mb_strtolower($expectedToCompare, 'UTF-8');
  53. $actualToCompare = mb_strtolower($actualToCompare, 'UTF-8');
  54. }
  55. }
  56. if ($expectedToCompare !== $actualToCompare && is_string($expected) && is_string($actual)) {
  57. [$cutExpected, $cutActual] = self::removeOverlongCommonPrefix($expected, $actual);
  58. [$cutExpected, $cutActual] = self::removeOverlongCommonSuffix($cutExpected, $cutActual);
  59. throw new ComparisonFailure(
  60. $expected,
  61. $actual,
  62. $exporter->export($cutExpected),
  63. $exporter->export($cutActual),
  64. 'Failed asserting that two strings are equal.',
  65. );
  66. }
  67. if ($expectedToCompare != $actualToCompare) {
  68. throw new ComparisonFailure(
  69. $expected,
  70. $actual,
  71. // no diff is required
  72. '',
  73. '',
  74. sprintf(
  75. 'Failed asserting that %s matches expected %s.',
  76. $exporter->export($actual),
  77. $exporter->export($expected),
  78. ),
  79. );
  80. }
  81. }
  82. /**
  83. * @return array{string, string}
  84. */
  85. private static function removeOverlongCommonPrefix(string $string1, string $string2): array
  86. {
  87. $commonPrefix = self::findCommonPrefix($string1, $string2);
  88. if (strlen($commonPrefix) > self::OVERLONG_THRESHOLD) {
  89. $string1 = '...' . substr($string1, strlen($commonPrefix) - self::KEEP_CONTEXT_CHARS);
  90. $string2 = '...' . substr($string2, strlen($commonPrefix) - self::KEEP_CONTEXT_CHARS);
  91. }
  92. return [$string1, $string2];
  93. }
  94. private static function findCommonPrefix(string $string1, string $string2): string
  95. {
  96. for ($i = 0; $i < strlen($string1); $i++) {
  97. if (!isset($string2[$i]) || $string1[$i] != $string2[$i]) {
  98. break;
  99. }
  100. }
  101. return substr($string1, 0, $i);
  102. }
  103. /**
  104. * @return array{string, string}
  105. */
  106. private static function removeOverlongCommonSuffix(string $string1, string $string2): array
  107. {
  108. $commonSuffix = self::findCommonSuffix($string1, $string2);
  109. if (strlen($commonSuffix) > self::OVERLONG_THRESHOLD) {
  110. $string1 = substr($string1, 0, -(strlen($commonSuffix) - self::KEEP_CONTEXT_CHARS)) . '...';
  111. $string2 = substr($string2, 0, -(strlen($commonSuffix) - self::KEEP_CONTEXT_CHARS)) . '...';
  112. }
  113. return [$string1, $string2];
  114. }
  115. private static function findCommonSuffix(string $string1, string $string2): string
  116. {
  117. if ($string1 === '' || $string2 === '') {
  118. return '';
  119. }
  120. $lastCharIndex1 = strlen($string1) - 1;
  121. $lastCharIndex2 = strlen($string2) - 1;
  122. if ($string1[$lastCharIndex1] != $string2[$lastCharIndex2]) {
  123. return '';
  124. }
  125. while (
  126. $lastCharIndex1 > 0 &&
  127. $lastCharIndex2 > 0 &&
  128. $string1[$lastCharIndex1] == $string2[$lastCharIndex2]
  129. ) {
  130. $lastCharIndex1--;
  131. $lastCharIndex2--;
  132. }
  133. return substr($string1, $lastCharIndex1 - strlen($string1) + 1);
  134. }
  135. }