diff --git a/README.md b/README.md index 1e74c81..c5c8d4a 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,11 @@ $version = Version::fromString('1.2.3-rc.1+exp.sha.5114f85'); ```php use SemVer\SemVer\Version; +use SemVer\SemVer\VersionComparator; $version1 = Version::fromString('1.2.3'); $version2 = Version::fromString('1.2.3-rc.1+exp.sha.5114f85'); -var_dump($version1->compare($version2)); // 1 +var_dump(VersionComparator::compare($version1, $version2)); // 1 var_dump($version1->equals($version2)); // false var_dump($version1->greaterThan($version2)); // true var_dump($version1->greaterThanOrEqual($version2)); // true diff --git a/Tests/VersionComparatorTest.php b/Tests/VersionComparatorTest.php new file mode 100644 index 0000000..13796de --- /dev/null +++ b/Tests/VersionComparatorTest.php @@ -0,0 +1,168 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace SemVer\SemVer\Tests; + +use PHPUnit_Framework_TestCase; +use SemVer\SemVer\Version; +use SemVer\SemVer\VersionComparator; + +/** + * Test of VersionComparator object. + */ +final class VersionComparatorTest extends PHPUnit_Framework_TestCase +{ + //////////////////////////////////////////////////////////////////////////// + // ::compare() + //////////////////////////////////////////////////////////////////////////// + + /** + * @dataProvider provideCompareVersions + * + * @param Version $version1 + * @param Version $version2 + * @param int $expectedResult + * @param string $message + */ + public function testCompare(Version $version1, Version $version2, int $expectedResult, string $message) + { + static::assertEquals($expectedResult, VersionComparator::compare($version1, $version2), $message); + } + + /** + * @return array + */ + public function provideCompareVersions() : array + { + return [ + // major + [ + Version::fromString('1.0.0'), + Version::fromString('2.0.0'), + -1, + '::compare() major version. 1.0.0 is lower than 2.0.0', + ], + [ + Version::fromString('2.0.0'), + Version::fromString('1.0.0'), + 1, + '::compare() major version. 2.0.0 is greater than 1.0.0', + ], + [ + Version::fromString('10.0.0'), + Version::fromString('2.0.0'), + 1, + '::compare() major version compares numerically. 10.0.0 is greater than 2.0.0', + ], + // minor + [ + Version::fromString('2.0.0'), + Version::fromString('2.10.0'), + -1, + '::compare() minor version. 2.0.0 is lower than 2.10.0', + ], + [ + Version::fromString('2.10.0'), + Version::fromString('2.0.0'), + 1, + '::compare() minor version. 2.10.0 is greater than 2.0.0', + ], + [ + Version::fromString('2.10.0'), + Version::fromString('2.2.0'), + 1, + '::compare() minor version compares numerically. 2.10.0 is greater than 2.2.0', + ], + // patch + [ + Version::fromString('2.0.0'), + Version::fromString('2.0.10'), + -1, + '::compare() patch version. 2.0.0 is lower than 2.0.10', + ], + [ + Version::fromString('2.0.10'), + Version::fromString('2.0.0'), + 1, + '::compare() patch version. 2.0.10 is greater than 2.0.0', + ], + [ + Version::fromString('2.0.10'), + Version::fromString('2.0.2'), + 1, + '::compare() patch version. 2.0.10 is greater than 2.0.2', + ], + [ + Version::fromString('2.0.0'), + Version::fromString('2.0.0+build'), + 0, + '::compare() build differs. versions should be equals.', + ], + // pre release + [ + Version::fromString('2.0.0'), + Version::fromString('2.0.0-alpha'), + 1, + '::compare() second version has a pre-release, the first not', + ], + [ + Version::fromString('2.0.0-alpha'), + Version::fromString('2.0.0'), + -1, + '::compare() first version has a pre-release, the second not', + ], + [ + Version::fromString('2.0.0-alpha.1'), + Version::fromString('2.0.0-alpha'), + 1, + '::compare() first has two pre-release identifiers, second only has one', + ], + [ + Version::fromString('2.0.0-alpha'), + Version::fromString('2.0.0-alpha.1'), + -1, + '::compare() second has two pre-release identifiers, first only has one', + ], + [ + Version::fromString('2.0.0-1'), + Version::fromString('2.0.0-beta'), + -1, + '::compare() a numeric identifier is lower than an alphabetical one', + ], + [ + Version::fromString('2.0.0-beta'), + Version::fromString('2.0.0-1'), + 1, + '::compare() an alphabetical identifier is greater than a numeric one', + ], + [ + Version::fromString('2.0.0-alpha.1'), + Version::fromString('2.0.0-alpha.beta'), + -1, + '::compare() a numeric identifier is lower than an alphabetical one even when multiple identifiers given', + ], + [ + Version::fromString('2.0.0-alpha.10'), + Version::fromString('2.0.0-alpha.2'), + 1, + '::compare() an alphabetical identifier is greater than a numeric one even when multiple identifiers given', + ], + [ + Version::fromString('2.0.0-alpha+build127'), + Version::fromString('2.0.0-alpha+build128'), + 0, + '::compare() two versions that only differs with their build are equals.', + ], + ]; + } +} diff --git a/Tests/VersionTest.php b/Tests/VersionTest.php index 978c5f0..c745034 100644 --- a/Tests/VersionTest.php +++ b/Tests/VersionTest.php @@ -241,137 +241,6 @@ public function testSort() } while (count($result) || count($expectedResult)); } - //////////////////////////////////////////////////////////////////////////// - // compare() - //////////////////////////////////////////////////////////////////////////// - - /** - * @dataProvider provideCompareVersions - * - * @param Version $version1 - * @param Version $version2 - * @param int $expectedResult - * @param string $message - */ - public function testCompare(Version $version1, Version $version2, int $expectedResult, string $message) - { - static::assertEquals($expectedResult, $version1->compare($version2), $message); - } - - /** - * @return array - */ - public function provideCompareVersions() : array - { - return [ - // major - [ - Version::fromString('1.0.0'), - Version::fromString('2.0.0'), - -1, - '::compare() versions must be ordered by major version (current lower than other)', - ], - [ - Version::fromString('2.0.0'), - Version::fromString('1.0.0'), - 1, - '::compare() versions must be ordered by major version (current greater than other)', - ], - [ - Version::fromString('2.0.0'), - Version::fromString('10.0.0'), - -1, - '::compare() versions must be ordered by major version numerically', - ], - // minor - [ - Version::fromString('2.10.0'), - Version::fromString('2.0.0'), - 1, - '::compare() if major versions are equals, then it must be ordered by minor version (current lower than other)', - ], - [ - Version::fromString('2.0.0'), - Version::fromString('2.10.0'), - -1, - '::compare() if major versions are equals, then it must be ordered by minor version (current greater than other)', - ], - [ - Version::fromString('2.10.0'), - Version::fromString('2.2.0'), - 1, - '::compare() if major versions are equals, then it must be ordered by minor version numerically', - ], - // patch - [ - Version::fromString('2.0.10'), - Version::fromString('2.0.0'), - 1, - '::compare() if major and minor versions are equals, then it must be ordered by patch version numerically (current lower than other)', - ], - [ - Version::fromString('2.0.0'), - Version::fromString('2.0.10'), - -1, - '::compare() if major and minor versions are equals, then it must be ordered by patch version numerically (current greater than other)', - ], - [ - Version::fromString('2.0.10'), - Version::fromString('2.0.2'), - 1, - '::compare() if major and minor versions are equals, then it must be ordered by patch version numerically', - ], - [ - Version::fromString('2.0.0'), - Version::fromString('2.0.0+build'), - 0, - '::compare() if major, minor and patch versions are equals and both versions do not have pre-release, then they are equals', - ], - [ - Version::fromString('2.0.0'), - Version::fromString('2.0.0-alpha'), - 1, - '::compare() When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version. (current without pre-release)', - ], - [ - Version::fromString('2.0.0-alpha'), - Version::fromString('2.0.0'), - -1, - '::compare() A larger set of pre-release fields has a higher precedence than a smaller set', - ], - [ - Version::fromString('2.0.0-alpha.1'), - Version::fromString('2.0.0-alpha'), - 1, - '::compare() A larger set of pre-release fields has a higher precedence than a smaller set (multiple level)', - ], - [ - Version::fromString('2.0.0-1'), - Version::fromString('2.0.0-beta'), - -1, - '::compare() Precedence for two pre-release versions with the same major, minor, and patch version. Numeric identifiers always have lower precedence than non-numeric identifiers', - ], - [ - Version::fromString('2.0.0-beta'), - Version::fromString('2.0.0-1'), - 1, - '::compare() Precedence for two pre-release versions with the same major, minor, and patch version. Numeric identifiers always have lower precedence than non-numeric identifiers', - ], - [ - Version::fromString('2.0.0-alpha.1'), - Version::fromString('2.0.0-alpha.beta'), - -1, - '::compare() Precedence for two pre-release versions with the same major, minor, and patch version. Numeric identifiers always have lower precedence than non-numeric identifiers. Test with multiple identifiers level.', - ], - [ - Version::fromString('2.0.0-alpha.10'), - Version::fromString('2.0.0-alpha.2'), - 1, - '::compare() numeric pre-release, minor, and patch version. Numeric identifiers always have lower precedence than non-numeric identifiers. Test with multiple identifiers level.', - ], - ]; - } - //////////////////////////////////////////////////////////////////////////// // __toString() //////////////////////////////////////////////////////////////////////////// diff --git a/Version.php b/Version.php index 8bd8ecc..de8c432 100644 --- a/Version.php +++ b/Version.php @@ -116,7 +116,7 @@ public static function sort(array $versions) : array usort( $versions, function (Version $a, Version $b) { - return $a->compare($b); + return VersionComparator::compare($a, $b); } ); @@ -163,72 +163,6 @@ public function getBuild() : string return $this->build; } - /** - * @param Version $other - * - * @return int - */ - public function compare(Version $other) : int - { - $compare = $this->major <=> $other->major; - if (0 !== $compare) { - return $compare; - } - $compare = $this->minor <=> $other->minor; - if (0 !== $compare) { - return $compare; - } - $compare = $this->patch <=> $other->patch; - if (0 !== $compare) { - return $compare; - } - - $myPreReleaseIsEmpty = '' === $this->preRelease; - $otherPreReleaseIsEmpty = '' === $other->preRelease; - if ($otherPreReleaseIsEmpty !== $myPreReleaseIsEmpty) { - return $myPreReleaseIsEmpty ? 1 : -1; - } - - if (!$myPreReleaseIsEmpty) { - // need to compare each subversion - $myPreRelease = explode('.', $this->preRelease); - $theirPreRelease = explode('.', $other->preRelease); - - do { - $myCurrentPreReleasePart = array_shift($myPreRelease); - $theirCurrentPreReleasePart = array_shift($theirPreRelease); - - $myCurrentPreReleasePartIsNull = null === $myCurrentPreReleasePart; - $theirCurrentPreReleasePartIsNull = null === $theirCurrentPreReleasePart; - - if ($myCurrentPreReleasePartIsNull !== $theirCurrentPreReleasePartIsNull) { - return $myCurrentPreReleasePartIsNull ? -1 : 1; - } - - $mineIsInt = ctype_digit($myCurrentPreReleasePart) && strpos($myCurrentPreReleasePart, '00') !== 0; - $theirIsInt = ctype_digit($theirCurrentPreReleasePart) && strpos( - $theirCurrentPreReleasePart, - '00' - ) !== 0; - - if ($mineIsInt !== $theirIsInt) { - return $mineIsInt ? -1 : 1; - } - if ($mineIsInt) { - $myCurrentPreReleasePart = (int) $myCurrentPreReleasePart; - $theirCurrentPreReleasePart = (int) $theirCurrentPreReleasePart; - } - - $compare = $myCurrentPreReleasePart <=> $theirCurrentPreReleasePart; - if (0 !== $compare) { - return $compare; - } - } while (count($myPreRelease) || count($theirPreRelease)); - } - - return 0; - } - /** * @param Version $other * @@ -236,7 +170,7 @@ public function compare(Version $other) : int */ public function equals(Version $other) : bool { - return 0 === $this->compare($other); + return 0 === VersionComparator::compare($this, $other); } /** @@ -246,7 +180,7 @@ public function equals(Version $other) : bool */ public function greaterThan(Version $other) : bool { - return 1 === $this->compare($other); + return 1 === VersionComparator::compare($this, $other); } /** @@ -256,7 +190,7 @@ public function greaterThan(Version $other) : bool */ public function greaterThanOrEqual(Version $other) : bool { - return 0 <= $this->compare($other); + return 0 <= VersionComparator::compare($this, $other); } /** @@ -266,7 +200,7 @@ public function greaterThanOrEqual(Version $other) : bool */ public function lessThan(Version $other) : bool { - return -1 === $this->compare($other); + return -1 === VersionComparator::compare($this, $other); } /** @@ -276,6 +210,6 @@ public function lessThan(Version $other) : bool */ public function lessThanOrEqual(Version $other) : bool { - return 0 >= $this->compare($other); + return 0 >= VersionComparator::compare($this, $other); } } diff --git a/VersionComparator.php b/VersionComparator.php new file mode 100644 index 0000000..599bc17 --- /dev/null +++ b/VersionComparator.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace SemVer\SemVer; + +/** + * Compare two Version objects. + */ +final class VersionComparator +{ + /** @var Version */ + private $first; + /** @var Version */ + private $second; + + /** + * @param Version $first + * @param Version $second + */ + private function __construct(Version $first, Version $second) + { + $this->first = $first; + $this->second = $second; + } + + /** + * @param Version $first + * @param Version $second + * + * @return int + */ + public static function compare(Version $first, Version $second) : int + { + $obj = new self($first, $second); + + return $obj->doCompare(); + } + + /** + * @return int + */ + private function doCompare() : int + { + $compare = $this->compareNumericParts(); + if (0 !== $compare) { + return $compare; + } + + return $this->comparePreRelease(); + } + + /** + * @return int + */ + private function compareNumericParts() : int + { + $compare = $this->first->getMajor() <=> $this->second->getMajor(); + if (0 !== $compare) { + return $compare; + } + $compare = $this->first->getMinor() <=> $this->second->getMinor(); + if (0 !== $compare) { + return $compare; + } + $compare = $this->first->getPatch() <=> $this->second->getPatch(); + + return $compare; + } + + /** + * @return int + */ + private function comparePreRelease() : int + { + $preRelease1 = $this->first->getPreRelease(); + $preRelease2 = $this->second->getPreRelease(); + + $leftPreReleaseIsEmpty = '' === $preRelease1; + $rightPreReleaseIsEmpty = '' === $preRelease2; + if ($rightPreReleaseIsEmpty !== $leftPreReleaseIsEmpty) { + return $leftPreReleaseIsEmpty ? 1 : -1; + } + + if ($leftPreReleaseIsEmpty) { + return 0; + } + + return $this->comparePreReleaseIdentifiers(explode('.', $preRelease1), explode('.', $preRelease2)); + } + + /** + * @param array $identifiers1 + * @param array $identifiers2 + * + * @return int + */ + private function comparePreReleaseIdentifiers(array $identifiers1, array $identifiers2) : int + { + $preReleasePart1 = array_shift($identifiers1); + $preReleasePart2 = array_shift($identifiers2); + if (null === $preReleasePart2) { + return (int) (null !== $preReleasePart1); + } + + if (null === $preReleasePart1) { + return -1; + } + + $compare = $this->comparePreReleaseIdentifier($preReleasePart1, $preReleasePart2); + if (0 === $compare) { + return $this->comparePreReleaseIdentifiers($identifiers1, $identifiers2); + } + + return $compare; + } + + /** + * @param $identifier1 + * @param $identifier2 + * + * @return int + */ + private function comparePreReleaseIdentifier($identifier1, $identifier2) : int + { + $mineIsInt = $this->isIdentifierInt($identifier1); + $theirIsInt = $this->isIdentifierInt($identifier2); + + if ($mineIsInt !== $theirIsInt) { + return $mineIsInt ? -1 : 1; + } + + if ($mineIsInt) { + return ((int) $identifier1) <=> ((int) $identifier2); + } + + return $identifier1 <=> $identifier2; + } + + /** + * @param string $identifier + * + * @return bool + */ + private function isIdentifierInt(string $identifier) : bool + { + return ctype_digit($identifier) && strpos($identifier, '00') !== 0; + } +}