From b6b0b3172c172f5795734348798cb6f4efbcd238 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 12 Apr 2022 09:20:59 +0300 Subject: [PATCH] initial --- README.md | 7 +- composer.json | 25 ++-- phpunit.xml | 19 ++- src/.gitkeep | 0 src/Comparator.php | 285 +++++++++++++++++++++++++++++++++++++ tests/.gitkeep | 0 tests/ComparatorTest.php | 138 ++++++++++++++++++ tests/MockedComparator.php | 34 +++++ 8 files changed, 481 insertions(+), 27 deletions(-) delete mode 100644 src/.gitkeep create mode 100644 src/Comparator.php delete mode 100644 tests/.gitkeep create mode 100644 tests/ComparatorTest.php create mode 100644 tests/MockedComparator.php diff --git a/README.md b/README.md index 08745f9..0d78628 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ -# dummy-package +# ensostudio/comparator -Repository template for Composer packages. +Package to comparing any values. # Installation +Via Composer: ```bash -composer require vendor/package +composer require ensostudio/comparator ``` \ No newline at end of file diff --git a/composer.json b/composer.json index ce850d6..662c498 100644 --- a/composer.json +++ b/composer.json @@ -1,34 +1,33 @@ { - "name": "vendor/package", - "description": "Repository template for Composer packages", - "keywords": ["package"], + "name": "ensostudio/comparator", + "description": "The flexible comparation of any values", + "keywords": ["compare", "comparation", "comparator", "php"], "license": "BSD-3-Clause", - "homepage": "https://github.com/vendor/package", + "homepage": "https://github.com/ensostudio/comparator", "authors": [ { - "name": "...", - "email": "...", + "name": "Anton Fedonyuk", + "email": "info@ensostudio.ru", "role": "developer" } ], "require": { - "php": ">=7.3" + "php": ">=7.2" }, "require-dev": { - "phpunit/phpunit": ">=8.5.25" + "phpunit/phpunit": "^8.5.25" }, "autoload": { "psr-4": { - "Vendor\\Package\\": "src/" + "EnsoStudio\\Comparator\\": "src/" } }, "autoload-dev": { "psr-4": { - "Vendor\\Package\\": "tests/" + "EnsoStudio\\Comparator\\": "tests/" } }, "config": { "optimize-autoloader": true - }, - "extra": {} -} \ No newline at end of file + } +} diff --git a/phpunit.xml b/phpunit.xml index 92db6fe..e80198e 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,17 +1,14 @@ - + - + tests - \ No newline at end of file diff --git a/src/.gitkeep b/src/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Comparator.php b/src/Comparator.php new file mode 100644 index 0000000..eae8d6f --- /dev/null +++ b/src/Comparator.php @@ -0,0 +1,285 @@ +setFlags($flags); + } + + /** + * Sets the behavior flags. + * + * @param int $flags The flags to control the comparasion behavior. It takes on either a bitmask, or self constants. + * @return void + */ + public function setFlags(int $flags) + { + $this->flags = max(0, $flags); + } + + /** + * Gets the behavior flags. + * + * @return int + */ + public function getFlags(): int + { + return $this->flags; + } + + /** + * Checks if the given behavior flag is set by default. + * + * @param int $flag The behavior flag to check. + * @return bool + */ + public function hasFlag(int $flag): bool + { + return ($this->flags & $flag) === $flag; + } + + /** + * Compares two values. + * + * @param mixed $value the first value to compare + * @param mixed $value2 the second value to compare + * @return bool + */ + public function compare($value, $value2): bool + { + if ($value === $value2 || (!$this->hasFlag(self::STRICT) && $value == $value2)) { + return true; + } + + $type = gettype($value); + $type2 = gettype($value2); + if ($this->hasFlag(self::STRICT) && $type !== $type2) { + return false; + } + if ($type === 'double' || $type2 === 'double') { + $type = 'float'; + } elseif ($type === 'string' || $type2 === 'string') { + $type = 'string'; + } + if ( + ($type === $type2 && in_array($type, ['array', 'object', 'resource'], true)) + || in_array($type, ['float', 'string'], true) + ) { + return $this->{'compare' . $type . 's'}($value, $value2); + } + + return false; + } + + /** + * Compares two decimal numbers. + * + * @param float $number the first number to compare + * @param float $number2 the second number to compare + * @return bool + */ + protected function compareFloats(float $number, float $number2): bool + { + $isNan = is_nan($number); + $isNan2 = is_nan($number2); + if ($isNan || $isNan2) { + return $isNan && $isNan2 && $this->hasFlag(self::EQUAL_NAN); + } + + if ($this->hasFlag(self::EQUAL_FLOAT)) { + return abs($number - $number2) < PHP_FLOAT_EPSILON + || (min($number, $number2) + PHP_FLOAT_EPSILON === max($number, $number2) - PHP_FLOAT_EPSILON); + } + + return false; + } + + /** + * Compares two strings. + * + * @param string $string the first string to compare + * @param string $string2 the second string to compare + * @return bool + */ + protected function compareStrings(string $string, string $string2): bool + { + $diff = $this->hasFlag(self::EQUAL_STRING) ? strcasecmp($string, $string2) : strcmp($string, $string2); + + return $diff === 0; + } + + /** + * Compares two arrays. + * + * @param array $array the first array to compare + * @param array $array2 the second array to compare + * @return bool + */ + protected function compareArrays(array $array, array $array2): bool + { + if (count($array) !== count($array2)) { + return false; + } + + if ($this->hasFlag(self::EQUAL_ARRAY)) { + ksort($array); + ksort($array2); + } + $keys = array_keys($array); + if ($keys != array_keys($array2)) { + return false; + } + + if ($this->hasFlag(self::EQUAL_INDEX_ARRAY) && $keys === array_keys($keys)) { + // sort values in index arrays + sort($array); + sort($array2); + } + + foreach ($array as $key => $value) { + if (!$this->compare($value, $array2[$key])) { + return false; + } + } + + return true; + } + + /** + * Compares two resources. + * + * @param resource $resource the first resource to compare + * @param resource $resource2 the second resource to compare + * @return bool + */ + protected function compareResources($resource, $resource2): bool + { + if ($this->hasFlag(self::EQUAL_STREAM)) { + $type = get_resource_type($resource); + if ($type === 'stream' && $type === get_resource_type($resource2)) { + return $this->compareArrays(stream_get_meta_data($resource), stream_get_meta_data($resource2)); + } + } + + return false; + } + + /** + * Compares two objects. + * + * @param object $object the first object to compare + * @param object $object2 the second object to compare + * @return bool + */ + protected function compareObjects(object $object, object $object2): bool + { + if (get_class($object) !== get_class($object2)) { + return false; + } + + if ($object instanceof Closure) { + if ($this->hasFlag(self::EQUAL_CLOSURE)) { + $rf = new ReflectionFunction($object); + $rf2 = new ReflectionFunction($object2); + $scope = $rf->getClosureThis(); + $scope2 = $rf2->getClosureThis(); + + return ($this->hasFlag(self::EQUAL_OBJECT) ? $scope == $scope2 : $scope === $scope2) + && (string) $rf === (string) $rf2; + } + + return false; + } + + if ($this->hasFlag(self::EQUAL_OBJECT)) { + return method_exists($object, '__toString') + ? $this->compareStrings((string) $object, (string) $object2) + : $this->compareArrays(get_object_vars($object), get_object_vars($object2)); + } + + return false; + } +} diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/ComparatorTest.php b/tests/ComparatorTest.php new file mode 100644 index 0000000..68645ad --- /dev/null +++ b/tests/ComparatorTest.php @@ -0,0 +1,138 @@ +assertSame(Comparator::STRICT, $comparator->getFlags()); + $comparator->setFlags(Comparator::EQUAL_CLOSURE | Comparator::EQUAL_FLOAT); + $this->assertSame(Comparator::EQUAL_CLOSURE | Comparator::EQUAL_FLOAT, $comparator->getFlags()); + } + + public function testHasFlag() + { + $comparator = new Comparator(Comparator::EQUAL_CLOSURE | Comparator::EQUAL_OBJECT); + $this->assertTrue($comparator->hasFlag(Comparator::EQUAL_CLOSURE)); + $this->assertFalse($comparator->hasFlag(Comparator::EQUAL_FLOAT)); + } + + public function testCompareResources() + { + $res = \fopen('https://www.php.net', 'r'); + $res2 = \fopen('https://www.php.net', 'r'); + + + $comparator = new MockedComparator(Comparator::STRICT); + $this->assertFalse($comparator->compareResources(\tmpfile(), \tmpfile())); + $this->assertFalse($comparator->compareResources($res, $res2)); + + $comparator->setFlags(Comparator::EQUAL_STREAM); + $this->assertTrue($comparator->compareResources($res, $res2)); + $this->assertFalse($comparator->compareResources($res, \fopen('https://www.php.net/support', 'r'))); + } + + public function testCompareFloats() + { + $nan = \acos(1.01); + $comparator = new MockedComparator(Comparator::STRICT); + $this->assertFalse($comparator->compareFloats(0.6, 3 - 2.4)); + $this->assertFalse($comparator->compareFloats($nan, $nan)); + + $comparator->setFlags(Comparator::EQUAL_FLOAT); + $this->assertTrue($comparator->compareFloats(0.6, 3 - 2.4)); + $comparator->setFlags(Comparator::EQUAL_NAN); + $this->assertTrue($comparator->compareFloats($nan, $nan)); + } + + public function testCompareStrings() + { + $comparator = new MockedComparator(Comparator::STRICT); + $this->assertFalse($comparator->compareStrings('foo', 'bar')); + $this->assertFalse($comparator->compareStrings('foo', 'Foo')); + $this->assertTrue($comparator->compareStrings('мой тест', 'мой тест')); + + $comparator->setFlags(Comparator::EQUAL_STRING); + $this->assertFalse($comparator->compareStrings('foo', 'bar')); + $this->assertTrue($comparator->compareStrings('foo', 'FoO')); + $this->assertFalse($comparator->compareStrings('мой тест', 'мой ТЕСТ')); + } + + public function testCompareArrays() + { + $comparator = new MockedComparator(Comparator::STRICT); + $this->assertFalse($comparator->compareArrays(['foo', 'bar'], ['bar', 'foo'])); + $this->assertFalse($comparator->compareArrays(['b' => [1, 2], 'a' => 0], ['a' => 0, 'b' => [1, 2]])); + + $comparator->setFlags(Comparator::EQUAL_ARRAY); + $this->assertFalse($comparator->compareArrays(['foo', 'bar'], ['bar', 'foo'])); + $this->assertTrue($comparator->compareArrays(['b' => [1, 2], 'a' => 0], ['a' => 0, 'b' => [1, 2]])); + + $comparator->setFlags(Comparator::EQUAL_INDEX_ARRAY); + $this->assertTrue($comparator->compareArrays(['foo', 'bar'], ['bar', 'foo'])); + } + + public function testCompareObjects() + { + $stdObject = (object) ['foo' => 'bar', 'baZz' => true]; + $stdObject2 = (object) ['foo' => 'bar', 'baZz' => true]; + $dateTime = new \DateTime('21-05-2021 11:30'); + $dateTime2 = new \DateTime('21-05-2021 11:30'); + $createClosure = function () { + return function () { + return 1; + }; + }; + $closure = $createClosure(); + $closure2 = $createClosure(); + + + $comparator = new MockedComparator(Comparator::EQUAL_OBJECT); + $this->assertTrue($comparator->compareObjects($stdObject, $stdObject)); + $this->assertTrue($comparator->compareObjects($stdObject, $stdObject2)); + $this->assertTrue($comparator->compareObjects($dateTime, $dateTime2)); + + $comparator->setFlags(Comparator::EQUAL_CLOSURE); + $this->assertTrue($comparator->compareObjects($closure, clone $closure)); + $this->assertTrue($comparator->compareObjects($closure, $closure2)); + } + + /** + * @depends testCompareFloats + * @depends testCompareStrings + * @depends testCompareResources + * @depends testCompareArrays + * @depends testCompareObjects + */ + public function testCompare() + { + $createClosure = function () { + return function () { + return 1; + }; + }; + + $comparator = new Comparator(); + $this->assertTrue($comparator->compare( + [ + 'closure' => $createClosure(), + 'arrayObject' => new \ArrayObject(['foo' => 1, 'bar' => 2]), + 'float' => 0.4, + 'str' => 'тест' + ], + [ + 'closure' => $createClosure(), + 'arrayObject' => new \ArrayObject(['bar' => 2,'foo' => 1]), + 'str' => 'тест', + 'float' => 3 - 2.6 + ] + )); + } +} diff --git a/tests/MockedComparator.php b/tests/MockedComparator.php new file mode 100644 index 0000000..52c6360 --- /dev/null +++ b/tests/MockedComparator.php @@ -0,0 +1,34 @@ +