JavaScript 版本号排序实现

实现版本号数组的排序,包括字符串比较法、大数加权法和循环比较法

问题

对版本号数组进行排序,例如将 ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5'] 排序为 ['4.3.5', '4.3.4.5', '2.3.3', '0.302.1', '0.1.1']

解答

字符串比较法

当版本号的每个子版本都是一位数字时,可以直接使用字符串比较:

const arr = ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5'];
arr.sort((a, b) => a > b ? -1 : 1);
console.log(arr); // ['4.3.5', '4.3.4.5', '2.3.3', '0.302.1', '0.1.1']

这种方法利用了字符串比较的特性:字符串比较基于 Unicode 值,逐位进行。. 的 Unicode 值为 46,0 为 48,其他数字依次递增。因此 '10.1' 大于 '1.1'

但这种方法有局限性。如果版本号包含多位数字,比较结果会出错:

const arr = ['0.5.1', '0.302.1'];
arr.sort((a, b) => a > b ? -1 : 1);
// 结果:['0.5.1', '0.302.1'],但期望是 ['0.302.1', '0.5.1']

大数加权法

对于遵循 MAJOR.MINOR.PATCH 规则的版本号,可以使用加权计算:

const arr = ['2.3.3', '4.3.4', '0.3.1'];
const p = 1000;

const gen = (version) => 
    version.split('.').reduce((acc, value, index, arr) => 
        acc + (+value) * Math.pow(p, arr.length - index - 1), 0);

arr.sort((a, b) => gen(b) - gen(a));
console.log(arr);

其中 p 是常量,需要大于任何子版本号至少一个量级,避免加权后产生污染。

对于不规则长度的版本号,先获取最大长度:

const arr = ['1.1', '2.3.3', '4.3.5', '0.302.1', '4.20.0', '4.3.5.1', '1.2.3.4.5'];
const p = 1000;

const maxLen = Math.max(...arr.map(item => item.split('.').length));

const gen = (version) =>
    version.split('.').reduce((acc, value, index) => 
        acc + (+value) * Math.pow(p, maxLen - index - 1), 0);

arr.sort((a, b) => gen(b) - gen(a));
console.log(arr);

注意:如果版本号位数很多且值很大,可能会超出 JavaScript number 类型的精度范围,可以考虑使用 BigInt。

循环比较法

适用性最强的方法,逐位比较子版本号:

arr.sort((a, b) => {
    let i = 0;
    const arr1 = a.split('.');
    const arr2 = b.split('.');

    while (true) {
        const s1 = arr1[i];
        const s2 = arr2[i++];

        if (s1 === undefined || s2 === undefined) {
            return arr2.length - arr1.length;
        }

        if (s1 === s2) continue;

        return s2 - s1;
    }
});

console.log(arr);

这种方法的逻辑是:如果当前位相同则比较下一位,如果某个版本号已经没有更多位数,则认为位数多的版本号更大。

关键点

  • 字符串比较法仅适用于所有子版本号都是一位数字的情况,因为字符串比较是基于 Unicode 值逐位进行的
  • 大数加权法中的常量 p 必须大于任何子版本号至少一个量级,避免加权计算时产生污染
  • 循环比较法适用性最强,可以处理任意格式的版本号,通过逐位比较数值大小来判断
  • 使用 split('.') 将版本号字符串转换为数组后,需要将字符串转为数字进行比较
  • 大数加权法在极端情况下可能溢出,未来可以使用 BigInt 解决精度问题