Last active
June 23, 2025 01:12
-
-
Save sybrew/fd5b447d1a9ccd4a3344d8267828d7a1 to your computer and use it in GitHub Desktop.
Semver sorter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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