Skip to content

Instantly share code, notes, and snippets.

@Johannestegner
Last active October 8, 2020 08:23
Show Gist options
  • Save Johannestegner/d71a3c1593d917562c4f449340f57c50 to your computer and use it in GitHub Desktop.
Save Johannestegner/d71a3c1593d917562c4f449340f57c50 to your computer and use it in GitHub Desktop.
SEMVER diff with pre-release.
const test = /(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:[.+~-](?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
const prerelease = /(?<type>[a-zA-Z]+)(?:[+~.-]?)(?<version>.*)/;
const groups = ['major', 'minor', 'patch'];
const preReleases = ['alpha', 'beta', 'rc', ''];
/**
* Test semver package versions.
*
* If v1 is higher version than v2, a positive integer will be returned.
* If v2 is higher version than v1, a negative integer will be returned.
* If v1 and v2 are equal, a 0 integer will be returned.
*
* Pre-releases are diffed as following:
*
* alpha is less than beta.
* beta is less than rc.
* rc is less than empty value.
*
* Pre-release versions (that is, <type>-x.x.x) can be separated by the following characters:
*
* -, ~, +, . and empty value.
*
* Pre-release versions are parsed as following:
*
* Integers are value matches (1 is larger than 2, etc).
* Strings is matched with standard string check (basically 'a' < 'b').
* If all matches but one string is longer, it will be seen as a higher version.
* If all matches, the package is equal in version.
*
* @param {string} v1
* @param {string} v2
* @return {number}
*/
const diff = (v1, v2) => {
const group1 = v1.match(test).groups;
const group2 = v2.match(test).groups;
for (const type of groups) {
const val = parseInt(group1[type], 10) - parseInt(group2[type], 10);
// If value is not equal, one of the packages are higher version.
if (val !== 0) {
return val;
}
}
return diffPreRelease(group1, group2);
};
/**
*
* @param {Object|{prerelease: string}} group1
* @param {Object|{prerelease: string}} group2
* @return {number}
*/
const diffPreRelease = (group1, group2) => {
// If one of the packages are _not_ a pre-release, it's higher version.
if (group1.prerelease === undefined) {
return 1;
} else if (group2.prerelease === undefined) {
return -1;
}
const pre1 = group1.prerelease.match(prerelease).groups;
const pre2 = group2.prerelease.match(prerelease).groups;
// If there is a diff in pre-release type (that is, alpha vs beta), check which is latest type.
const typeDiff = preReleases.indexOf(pre1.type.toLowerCase()) - preReleases.indexOf(pre2.type.toLowerCase());
if (typeDiff !== 0) {
return typeDiff;
}
// Check each version entry of the pre-release. Could basically be arbitrary characters, we skip all `.-~+`.
const preV1 = pre1.version.replace(/[-.~+]/, '');
const preV2 = pre2.version.replace(/[-.~+]/, '');
let index = Math.min(preV1.length, preV2.length);
for (let i = 0; i < index; i++) {
// If a version is higher, return diff.
if (pre1.version[i] !== pre2.version[i]) {
return pre1.version[i] - pre2.version[i];
}
}
// At this point, most of the values matches, but one pre-release version might be a longer string.
// So if one is longer than the other, return that as higher value, else they match.
return pre1.version.length > pre2.version.length ? 1 : pre1.version.length < pre2.version.length ? -1 : 0;
}
module.exports = {
diff
};
@Johannestegner
Copy link
Author

Johannestegner commented Oct 8, 2020

Test:

const { diff } = require('./semver');

const list = [
  ['1.0.3', '1.1.7-rc.1'],
  ['v1.1.7-beta.1', '1.1.7-rc.1'],
  ['1.1.7-alpha.1', '1.1.7-rc.1'],
  ['1.1.7-beta.1', '1.1.7-alpha.1'],
  ['v1.1.7-beta.7', '1.1.7-rc.1'],
  ['v1.1.7-rc.7', 'v1.1.7-rc.1'],
  ['v1.1.7-rc7x.Z', 'v1.1.7-rc7'],
  ['v1.1.7-rc7x.Z', 'v1.1.7-rc.7'],
  ['v1.1.7-rc.7x.Z', 'v1.1.7-rc+7']
];

list.forEach((inner) => {
  console.log('Diff between %s and %s is %d', inner[0], inner[1], diff(inner[0], inner[1]));
  console.log('Diff between %s and %s is %d', inner[1], inner[0], diff(inner[1], inner[0]));
});

Result:

Diff between 1.0.3 and 1.1.7-rc.1 is -1
Diff between 1.1.7-rc.1 and 1.0.3 is 1
Diff between v1.1.7-beta.1 and 1.1.7-rc.1 is -1
Diff between 1.1.7-rc.1 and v1.1.7-beta.1 is 1
Diff between 1.1.7-alpha.1 and 1.1.7-rc.1 is -2
Diff between 1.1.7-rc.1 and 1.1.7-alpha.1 is 2
Diff between 1.1.7-beta.1 and 1.1.7-alpha.1 is 1
Diff between 1.1.7-alpha.1 and 1.1.7-beta.1 is -1
Diff between v1.1.7-beta.7 and 1.1.7-rc.1 is -1
Diff between 1.1.7-rc.1 and v1.1.7-beta.7 is 1
Diff between v1.1.7-rc.7 and v1.1.7-rc.1 is 6
Diff between v1.1.7-rc.1 and v1.1.7-rc.7 is -6
Diff between v1.1.7-rc7x.Z and v1.1.7-rc7 is 1
Diff between v1.1.7-rc7 and v1.1.7-rc7x.Z is -1
Diff between v1.1.7-rc7x.Z and v1.1.7-rc.7 is 1
Diff between v1.1.7-rc.7 and v1.1.7-rc7x.Z is -1
Diff between v1.1.7-rc.7x.Z and v1.1.7-rc+7 is 1
Diff between v1.1.7-rc+7 and v1.1.7-rc.7x.Z is -1

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