Skip to content

Instantly share code, notes, and snippets.

@sybrew
Last active April 6, 2025 21:56
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 version 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.
*
* @param {Array<Object>} versions - An array of version objects where each object contains a "version" property of type string.
* @returns {Array<Object>} - A new array of version objects sorted in ascending order based on semantic versioning.
*/
function sortVersions( versions = [] ) {
// 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-]+)*))?$/;
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 );
const mapping = { dev: 0, alpha: 1, a: 1, beta: 2, b: 2, rc: 3 };
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( mapping[ letter ] );
prerelease.push( numberPart ? parseInt( numberPart, 10 ) : 0 );
} else {
prerelease.push( -1 );
}
} );
} else {
prerelease.push( 4 ); // Final release indicator (higher than pre-releases)
}
const build = match[ 5 ] ? match[ 5 ].split( '.' ).map( s => parseInt( s, 10 ) ) : [];
return [ major, minor, patch, ...prerelease, ...build ];
}
return versions.sort( ( a, b ) => {
const versionA = parseVersion( a.version );
const versionB = parseVersion( b.version );
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', 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