Skip to content

Instantly share code, notes, and snippets.

@gmazzap
Created March 24, 2019 19:37
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gmazzap/05e1d162b9b5eaed47d2bbad013847ab to your computer and use it in GitHub Desktop.
Save gmazzap/05e1d162b9b5eaed47d2bbad013847ab to your computer and use it in GitHub Desktop.
Version number parser and comparator compatible with both Semver and WordPress version numbers.
<?php
/**
* Copyright 2019 Inpsyde GmbH
*
* @license MIT https://opensource.org/licenses/MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
declare(strict_types=1);
namespace Inpsyde\Semver;
/**
* Version number parser and comparator compatible with both Semver and WordPress version numbers.
*
* Works by "normalizing" any version string to a Semver-compatible version string, and compare
* those when asked to.
*
* @license
*/
class VersionNumber
{
/**
* @var string
*/
private $version;
/**
* @param string $v1
* @param string $v2
* @return int Either 1, 0 or -1 as in `version_compare`
*/
public static function compareVersions(string $v1, string $v2): int
{
$left = explode('-', (string)(new static($v1)), 2);
$right = explode('-', (string)(new static($v2)), 2);
$compare = version_compare($left[0], $right[0]);
// If "numbers" are different, no need to check dev part
if ($compare !== 0) {
return $compare;
}
// left is stable, right is dev: left > right
if (empty($left[1]) && !empty($right[1])) {
return 1;
}
// left is dev, right is stable: left < right
if (!empty($left[1]) && empty($right[1])) {
return -1;
}
// both are dev
return version_compare('1.' . $left[1], '1.' . $right[1]);
}
/**
* @param string $version
*/
public function __construct(string $version)
{
$this->version = $this->normalize($version);
}
/**
* @param VersionNumber
* @return int Either 1, 0 or -1 as in version_compare
*/
public function compareTo(VersionNumber $version): int
{
return static::compareVersions((string)$this, (string)$version);
}
/**
* @return string
*/
public function __toString(): string
{
return $this->version;
}
/**
* Formats the given number according to the semantic version specification.
*
* @param string $version
* @return string
*
* @see http://semver.org/#semantic-versioning-specification-semver
*/
private function normalize(string $version): string
{
list($number, $preRelease, $meta) = $this->matchSemverPattern($version);
if (!$number) {
throw new \Exception("Could not extract a valid version from {$version}.");
}
$version = $number;
if ($preRelease) {
$version .= "-{$preRelease}";
}
if ($meta) {
$version .= "+{$meta}";
}
return $version;
}
/**
* Returns a 3 items array with the 3 parts of SemVer specs, in order:
* - The numeric part of SemVer specs
* - The pre-release part of SemVer specs, could be empty
* - The meta part of SemVer specs, could be empty.
*
* @param string $version
* @return string[]
*/
private function matchSemverPattern(string $version): array
{
$pattern = '~^(?P<numbers>(?:[0-9]+)+(?:[0-9\.]+)?)+(?P<anything>.*?)?$~';
$matched = preg_match($pattern, $version, $matches);
if (!$matched) {
return ['', '', ''];
}
$numbers = explode('.', trim($matches['numbers'], '.'));
// if less than 3 numbers, ensure at least 3 numbers, filling with zero
$numeric = implode(
'.',
array_replace(
['0', '0', '0'],
array_slice($numbers, 0, 3)
)
);
// if more than 3 numbers, store additional numbers as build.
$build = implode('.', array_slice($numbers, 3));
// if there's nothing else, we already know what to return.
if (!$matches['anything']) {
return [$numeric, $build, ''];
}
$pre = ltrim($matches['anything'], '-');
$meta = '';
// seems we have some metadata.
if (substr_count($matches['anything'], '+') > 0) {
$parts = explode('+', $pre);
// pre is what's before the first +, which could actually be empty
// when version has meta but not pre-release.
$pre = array_shift($parts);
// everything comes after first + is meta.
// If there were more +, we replace them with dots.
$meta = $this->sanitizeIdentifier(trim(implode('.', $parts), '-'));
}
if ($build) {
$pre = "{$build}.{$pre}";
}
return [$numeric, $this->sanitizeIdentifier($pre), $meta];
}
/**
* Sanitizes given identifier according to SemVer specs.
* Allow for underscores, replacing them with hyphens.
*
* @param string $identifier
* @return string
*/
private function sanitizeIdentifier(string $identifier): string
{
// the condition will be false for both "" and "0", which are both valid
// so don't need any replace.
if ($identifier) {
$identifier = (string)preg_replace(
'~[^a-zA-Z0-9\-\.]~',
'',
str_replace('_', '-', $identifier)
);
}
return $identifier;
}
}
@gmazzap
Copy link
Author

gmazzap commented Mar 24, 2019

Usage

Normalization

$version = new VersionNumber('5.2-alpha-44742-src');
echo (string)$version;   // echoes "5.2.0-alpha-44742-src" which is valid Semver

Comparison

Using strings:

$version1 = '5.2-alpha-44742-src';
$version2 = '5.2.0';

var_dump(VersionNumber::compareVersions($version1, $version2)); // int(-1)

VersionNumber::compareVersions() returns either 0, 1 or -1 following same rules as version_compare.

Using objects:

$version1 = new VersionNumber('5.2-alpha-44742-src');
$version2 = new VersionNumber('5.2.0');

var_dump($version1->compareTo($version2)); // int(-1)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment