Skip to content

Instantly share code, notes, and snippets.

@sybrew
Last active June 23, 2025 01:12
Show Gist options
  • Save sybrew/fd5b447d1a9ccd4a3344d8267828d7a1 to your computer and use it in GitHub Desktop.
Save sybrew/fd5b447d1a9ccd4a3344d8267828d7a1 to your computer and use it in GitHub Desktop.
Semver sorter
/**
* Sorts an array of version objects based on their semantic version numbers,
* including pre-release versions.
*
* This function parses each version string using a regular expression to
* extract its numeric, pre-release, and build components and then compares
* them to order the array.
*
* It supports complex versions such as (in order) "1.4.9", "1.5.0-alpha",
* "1.5.0-alpha2", and "1.5.0", ensuring that pre-release versions are sorted
* correctly relative to final releases.
*
* 1.1 is not allowed. Write 1.1.0 instead.
*
* @see https://github.com/php/php-src/blob/php-8.4.8/ext/standard/versioning.c#L87-L99
*
* @param {Array<Object>} versions An array of objects, each containing a
* version string property (index).
* @param {string} index The property name to use for extracting
* the version string from each object.
* @return {Array<Object>} The sorted array of version objects.
*/
function sortVersions( versions = [], index = 'version' ) {
// Copied from https://semver.org/
const versionRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
/**
* Mapping of pre-release identifiers to their respective numeric values.
* This is used to ensure that pre-release versions are sorted correctly.
*
* - unknown identifiers are considered even earlier than `dev`.
* - `dev` and `alpha` are considered the earliest pre-release versions.
* - `beta` and `b` are considered the middle pre-release versions.
* - `rc` and `a` are considered the latest pre-release versions.
* - Final releases (no pre-release identifier) are considered latest.
*/
const versionMapping = {
dev: 0,
alpha: 1,
a: 1,
beta: 2,
b: 2,
rc: 3,
'#': 4,
pl: 5,
p: 5,
_unknown: 6,
};
const parseVersion = ( version ) => {
const match = version.match( versionRegex );
if ( ! match ) return [ 0, 0, 0, 0 ];
const major = parseInt( match[ 1 ], 10 );
const minor = parseInt( match[ 2 ], 10 );
const patch = parseInt( match[ 3 ], 10 );
let prerelease = [];
if ( match[ 4 ] ) {
const parts = match[ 4 ].split( '.' );
parts.forEach( part => {
const letterMatch = part.match( /^[a-zA-Z]+/ );
if ( letterMatch ) {
const letter = letterMatch[0].toLowerCase();
const numberPart = part.slice( letter.length );
prerelease.push( versionMapping?.[ letter ] || versionMapping._unknown );
prerelease.push( numberPart ? parseInt( numberPart, 10 ) : 0 );
} else {
prerelease.push( -1 );
}
} );
} else {
prerelease.push( 7 ); // Final release indicator (higher than pre-releases)
}
const build = match[ 5 ]?.split( '.' ).map( s => parseInt( s, 10 ) ) || [];
return [ major, minor, patch, ...prerelease, ...build ];
}
return versions.sort( ( a, b ) => {
const versionA = parseVersion( a[ index ] );
const versionB = parseVersion( b[ index ] );
for ( let i = 0; i < Math.max( versionA.length, versionB.length ); i++ )
if ( versionA[ i ] !== versionB[ i ] )
return versionA[ i ] - versionB[ i ];
return 0;
} );
}
// --- Test:
const exampleVersions = [
{ version: '1.5.3-beta', data: '1.5.3-beta' },
{ version: '1.5.3#123', data: '1.5.3#123' },
{ version: '1.5.3#55', data: '1.5.3#55' },
{ version: '1.5.3', data: '1.5.3' },
{ version: '1.5.2', data: '1.5.2' },
{ version: '1.4.9-beta', data: '1.4.9-beta' },
{ version: '1.4.9-beta2', data: '1.4.9-beta2' },
{ version: '1.4.9', data: '1.4.9' },
{ version: '1.4.9-rc1', data: '1.4.9-rc1' },
{ version: '1.5.1', data: '1.5.1' },
{ version: '1.5.3-alpha', data: '1.5.3-alpha' },
{ version: '1.5.0', data: '1.5.0' },
{ version: '1.4.9-rc', data: '1.4.9-rc' },
{ version: '1.4.8', data: '1.4.8' },
{ version: '1.4.7', data: '1.4.7' },
];
console.log( sortVersions(exampleVersions ) );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment