跳到主要内容

版本号比较

问题

实现一个版本号比较函数,比较两个版本号的大小。版本号由数字和 . 组成,如 1.0.01.2.3

答案

版本号比较是常见场景,如检测更新、兼容性判断等。


基础实现

/**
* 比较两个版本号
* @returns 1 表示 v1 > v2, -1 表示 v1 < v2, 0 表示相等
*/
function compareVersion(v1: string, v2: string): number {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
const maxLen = Math.max(parts1.length, parts2.length);

for (let i = 0; i < maxLen; i++) {
const num1 = parts1[i] || 0;
const num2 = parts2[i] || 0;

if (num1 > num2) return 1;
if (num1 < num2) return -1;
}

return 0;
}

// 测试
console.log(compareVersion('1.0.0', '1.0.0')); // 0
console.log(compareVersion('1.0.1', '1.0.0')); // 1
console.log(compareVersion('1.0.0', '1.0.1')); // -1
console.log(compareVersion('1.0', '1.0.0')); // 0
console.log(compareVersion('1.10.0', '1.9.0')); // 1 (不是按字符串比较)

进阶版本

支持预发布版本

版本格式:major.minor.patch[-prerelease][+build]

interface VersionInfo {
major: number;
minor: number;
patch: number;
prerelease: string[];
build: string[];
}

function parseVersion(version: string): VersionInfo {
// 分离 build metadata
const [versionAndPre, ...buildParts] = version.split('+');
const build = buildParts.length ? buildParts.join('+').split('.') : [];

// 分离 prerelease
const [versionStr, ...preParts] = versionAndPre.split('-');
const prerelease = preParts.length ? preParts.join('-').split('.') : [];

// 解析主版本号
const [major, minor = '0', patch = '0'] = versionStr.split('.');

return {
major: parseInt(major, 10),
minor: parseInt(minor, 10),
patch: parseInt(patch, 10),
prerelease,
build,
};
}

function comparePrerelease(pre1: string[], pre2: string[]): number {
// 没有 prerelease 的版本更大
if (pre1.length === 0 && pre2.length === 0) return 0;
if (pre1.length === 0) return 1;
if (pre2.length === 0) return -1;

const maxLen = Math.max(pre1.length, pre2.length);

for (let i = 0; i < maxLen; i++) {
// 字段少的版本更小
if (i >= pre1.length) return -1;
if (i >= pre2.length) return 1;

const part1 = pre1[i];
const part2 = pre2[i];

const isNum1 = /^\d+$/.test(part1);
const isNum2 = /^\d+$/.test(part2);

if (isNum1 && isNum2) {
// 都是数字,按数值比较
const num1 = parseInt(part1, 10);
const num2 = parseInt(part2, 10);
if (num1 !== num2) return num1 > num2 ? 1 : -1;
} else if (isNum1) {
// 数字 < 字符串
return -1;
} else if (isNum2) {
return 1;
} else {
// 都是字符串,按字典序
if (part1 !== part2) return part1 > part2 ? 1 : -1;
}
}

return 0;
}

function compareSemver(v1: string, v2: string): number {
const ver1 = parseVersion(v1);
const ver2 = parseVersion(v2);

// 比较主版本号
if (ver1.major !== ver2.major) return ver1.major > ver2.major ? 1 : -1;
if (ver1.minor !== ver2.minor) return ver1.minor > ver2.minor ? 1 : -1;
if (ver1.patch !== ver2.patch) return ver1.patch > ver2.patch ? 1 : -1;

// 比较预发布版本
return comparePrerelease(ver1.prerelease, ver2.prerelease);
}

// 测试
console.log(compareSemver('1.0.0', '1.0.1')); // -1
console.log(compareSemver('1.0.0-alpha', '1.0.0')); // -1 (预发布 < 正式)
console.log(compareSemver('1.0.0-alpha', '1.0.0-beta')); // -1 (alpha < beta)
console.log(compareSemver('1.0.0-alpha.1', '1.0.0-alpha.2')); // -1
console.log(compareSemver('1.0.0-1', '1.0.0-alpha')); // -1 (数字 < 字符串)

版本范围判断

function satisfies(version: string, range: string): boolean {
// 处理简单范围: ^, ~, >=, <=, >, <, =
const rangeRegex = /^(\^|~|>=?|<=?|=)?(.+)$/;
const match = range.match(rangeRegex);

if (!match) return false;

const [, operator = '=', rangeVersion] = match;

const cmp = compareSemver(version, rangeVersion);

switch (operator) {
case '=':
return cmp === 0;
case '>':
return cmp > 0;
case '>=':
return cmp >= 0;
case '<':
return cmp < 0;
case '<=':
return cmp <= 0;
case '^': {
// 兼容主版本号
const ver = parseVersion(version);
const rangeVer = parseVersion(rangeVersion);
if (ver.major !== rangeVer.major) return false;
return cmp >= 0;
}
case '~': {
// 兼容次版本号
const ver = parseVersion(version);
const rangeVer = parseVersion(rangeVersion);
if (ver.major !== rangeVer.major) return false;
if (ver.minor !== rangeVer.minor) return false;
return cmp >= 0;
}
default:
return false;
}
}

// 测试
console.log(satisfies('1.2.3', '>=1.0.0')); // true
console.log(satisfies('1.2.3', '^1.0.0')); // true
console.log(satisfies('2.0.0', '^1.0.0')); // false
console.log(satisfies('1.2.3', '~1.2.0')); // true
console.log(satisfies('1.3.0', '~1.2.0')); // false

版本排序

function sortVersions(versions: string[], ascending = true): string[] {
return [...versions].sort((a, b) => {
const cmp = compareSemver(a, b);
return ascending ? cmp : -cmp;
});
}

// 测试
const versions = ['1.0.0', '2.1.0', '1.10.0', '1.2.0', '0.9.0'];
console.log(sortVersions(versions));
// ['0.9.0', '1.0.0', '1.2.0', '1.10.0', '2.1.0']

console.log(sortVersions(versions, false));
// ['2.1.0', '1.10.0', '1.2.0', '1.0.0', '0.9.0']

版本工具类

class Version {
private major: number;
private minor: number;
private patch: number;
private prerelease: string[];

constructor(version: string) {
const parsed = parseVersion(version);
this.major = parsed.major;
this.minor = parsed.minor;
this.patch = parsed.patch;
this.prerelease = parsed.prerelease;
}

compare(other: Version | string): number {
const otherVersion = typeof other === 'string' ? new Version(other) : other;
return compareSemver(this.toString(), otherVersion.toString());
}

gt(other: Version | string): boolean {
return this.compare(other) > 0;
}

lt(other: Version | string): boolean {
return this.compare(other) < 0;
}

eq(other: Version | string): boolean {
return this.compare(other) === 0;
}

gte(other: Version | string): boolean {
return this.compare(other) >= 0;
}

lte(other: Version | string): boolean {
return this.compare(other) <= 0;
}

// 递增版本号
inc(type: 'major' | 'minor' | 'patch'): Version {
switch (type) {
case 'major':
return new Version(`${this.major + 1}.0.0`);
case 'minor':
return new Version(`${this.major}.${this.minor + 1}.0`);
case 'patch':
return new Version(`${this.major}.${this.minor}.${this.patch + 1}`);
}
}

toString(): string {
const base = `${this.major}.${this.minor}.${this.patch}`;
if (this.prerelease.length) {
return `${base}-${this.prerelease.join('.')}`;
}
return base;
}

static isValid(version: string): boolean {
const semverRegex = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/;
return semverRegex.test(version);
}
}

// 使用
const v1 = new Version('1.2.3');
const v2 = new Version('1.3.0');

console.log(v1.lt(v2)); // true
console.log(v1.inc('patch').toString()); // '1.2.4'
console.log(Version.isValid('1.0.0')); // true
console.log(Version.isValid('1.0')); // false

LeetCode 165: 比较版本号

// 符合 LeetCode 要求的实现
function compareVersionLeetCode(version1: string, version2: string): number {
const v1 = version1.split('.');
const v2 = version2.split('.');
const n = Math.max(v1.length, v2.length);

for (let i = 0; i < n; i++) {
const num1 = parseInt(v1[i] || '0', 10);
const num2 = parseInt(v2[i] || '0', 10);

if (num1 > num2) return 1;
if (num1 < num2) return -1;
}

return 0;
}

常见面试问题

Q1: 为什么不能直接用字符串比较?

答案

// ❌ 字符串比较会出错
'1.10.0' > '1.9.0'; // false,因为 '1' < '9'
'1.2.0' > '1.10.0'; // true,因为 '2' > '1'

// ✅ 应该按数字比较
compareVersion('1.10.0', '1.9.0'); // 1 (正确)

字符串按字典序逐字符比较,'10' 的第一个字符 '1' 小于 '9'

Q2: 语义化版本的规则是什么?

答案

版本号含义何时递增
Major主版本号不兼容的 API 变更
Minor次版本号向后兼容的新功能
Patch修订号向后兼容的 Bug 修复

预发布版本优先级:alpha < beta < rc < (正式版)

Q3: ^~ 的区别?

答案

符号含义示例
^兼容主版本号^1.2.3 匹配 1.x.x
~兼容次版本号~1.2.3 匹配 1.2.x
// ^1.2.3 允许:1.2.3, 1.2.4, 1.3.0, 1.9.9
// ^1.2.3 不允许:2.0.0

// ~1.2.3 允许:1.2.3, 1.2.4, 1.2.99
// ~1.2.3 不允许:1.3.0

相关链接