Semantic Version Comparison
Parse semantic versions and compare major, minor, patch, and prerelease identifiers according to SemVer precedence rules.
Code
export interface SemVersion {
readonly major: number;
readonly minor: number;
readonly patch: number;
readonly prerelease: ReadonlyArray<string | number>;
}
const SEMVER_PATTERN =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:[0-9A-Za-z-]+)(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
function parseIdentifier(identifier: string): string | number {
if (!/^\d+$/.test(identifier)) return identifier;
if (identifier.length > 1 && identifier.startsWith("0")) {
throw new Error(`invalid numeric identifier "${identifier}"`);
}
const value = Number(identifier);
if (!Number.isSafeInteger(value)) {
throw new Error(`identifier "${identifier}" is too large`);
}
return value;
}
export function parseSemVer(version: string): SemVersion {
if (typeof version !== "string") {
throw new TypeError("version must be a string");
}
const match = SEMVER_PATTERN.exec(version.trim());
if (match === null) {
throw new Error(`invalid semantic version: "${version}"`);
}
const major = Number(match[1]);
const minor = Number(match[2]);
const patch = Number(match[3]);
if (![major, minor, patch].every((value) => Number.isSafeInteger(value))) {
throw new Error("version numbers must be safe integers");
}
const prerelease =
match[4] === undefined
? []
: match[4].split(".").map(parseIdentifier);
return { major, minor, patch, prerelease };
}
function compareIdentifiers(
left: string | number,
right: string | number,
): -1 | 0 | 1 {
if (typeof left === "number" && typeof right === "number") {
if (left === right) return 0;
return left < right ? -1 : 1;
}
if (typeof left === "number") return -1;
if (typeof right === "number") return 1;
if (left === right) return 0;
return left < right ? -1 : 1;
}
export function compareSemVer(
left: SemVersion,
right: SemVersion,
): -1 | 0 | 1 {
if (left.major !== right.major) return left.major < right.major ? -1 : 1;
if (left.minor !== right.minor) return left.minor < right.minor ? -1 : 1;
if (left.patch !== right.patch) return left.patch < right.patch ? -1 : 1;
if (left.prerelease.length === 0 && right.prerelease.length === 0) {
return 0;
}
if (left.prerelease.length === 0) return 1;
if (right.prerelease.length === 0) return -1;
const length = Math.max(
left.prerelease.length,
right.prerelease.length,
);
for (let index = 0; index < length; index += 1) {
if (index >= left.prerelease.length) return -1;
if (index >= right.prerelease.length) return 1;
const result = compareIdentifiers(
left.prerelease[index],
right.prerelease[index],
);
if (result !== 0) return result;
}
return 0;
}
export function compareVersions(left: string, right: string): -1 | 0 | 1 {
return compareSemVer(parseSemVer(left), parseSemVer(right));
}
export function sortVersions(versions: readonly string[]): string[] {
return versions
.map((version) => ({ version, parsed: parseSemVer(version) }))
.sort((left, right) => compareSemVer(left.parsed, right.parsed))
.map((entry) => entry.version);
}
const versions = [
"1.0.0",
"1.0.0-alpha",
"1.0.0-alpha.1",
"1.0.0-beta.2",
"1.0.0-rc.1",
"1.0.1",
"2.0.0",
];
console.log(sortVersions(versions));
console.log(compareVersions("1.0.0+build.5", "1.0.0+build.9"));
console.log(compareVersions("1.0.0-alpha.10", "1.0.0-alpha.2"));
try {
parseSemVer("01.0.0");
} catch (error) {
console.log(error instanceof Error ? error.message : String(error));
}Report a problem with this exampleOpens a pre-filled issue on GitHub.