如何正确排序包含嵌入数字的字符串
使用数字排序以自然顺序对文件名、版本号和其他包含数字的字符串进行排序
介绍
当您对包含数字的字符串进行排序时,您期望 file1.txt、file2.txt 和 file10.txt 按此顺序出现。然而,JavaScript 的默认字符串比较会产生 file1.txt、file10.txt、file2.txt 的顺序。这是因为字符串是逐字符比较的,而 10 中的字符 1 排在字符 2 之前。
这个问题会在您对文件名、版本号、街道地址、产品代码或任何其他包含嵌入数字的字符串进行排序时出现。不正确的排序会让用户感到困惑,并使数据难以导航。
JavaScript 提供了带有数字选项的 Intl.Collator API,可以解决此问题。本课程将解释数字排序的工作原理、默认字符串比较为何失败,以及如何以自然的数字顺序对包含嵌入数字的字符串进行排序。
什么是数字排序
数字排序是一种将数字序列视为数字而非单个字符的比较方法。在比较字符串时,排序器会识别数字序列并按其数值进行比较。
在禁用数字排序的情况下,字符串 file10.txt 会排在 file2.txt 之前,因为逐字符比较发现第一个不同的位置中 1 排在 2 之前。排序器从未考虑到 10 表示的数字大于 2。
在启用数字排序的情况下,排序器会将 10 和 2 识别为完整的数字并按数值进行比较。由于 10 大于 2,file2.txt 会正确地排在 file10.txt 之前。
这种行为产生了人们称之为自然排序或自然顺序的结果,其中包含数字的字符串按照人类期望的方式排序,而不是严格按字母顺序排序。
默认字符串比较为何对数字无效
JavaScript 默认的字符串比较使用字典序排序,即从左到右逐字符地根据 Unicode 码点值进行比较。这种方法对字母文本的排序是正确的,但对数字会产生意外的结果。
请看字典序比较如何处理以下字符串:
const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];
files.sort();
console.log(files);
// 输出: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']
比较会独立检查每个字符位置。在 file 之后的第一个不同字符位置,它会比较 1 和 2。由于 1 的 Unicode 值小于 2,因此任何以 file1 开头的字符串都会排在以 file2 开头的字符串之前,而不考虑后续字符。
这就产生了 file1.txt、file10.txt、file2.txt、file20.txt 的顺序,这与人类对数字排序的预期不符。
使用带有 numeric 选项的 Intl.Collator
Intl.Collator 构造函数接受一个包含 numeric 属性的选项对象。将 numeric: true 设置为启用数字排序,使得比较器根据数字值比较数字序列。
const collator = new Intl.Collator('en-US', { numeric: true });
const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];
files.sort(collator.compare);
console.log(files);
// 输出: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']
比较器的 compare 方法在第一个参数应排在第二个参数之前时返回负数,在两者相等时返回零,在第一个参数应排在第二个参数之后时返回正数。这与 JavaScript 的 Array.sort() 方法所期望的签名相匹配。
排序结果将文件按自然的数字顺序排列。比较器识别出 1 < 2 < 10 < 20,从而生成符合人类预期的顺序。
排序混合字母数字字符串
数字排序可以处理字符串中任意位置出现的数字,而不仅仅是末尾。排序器会正常比较字母部分,并以数字方式比较数字部分。
const collator = new Intl.Collator('en-US', { numeric: true });
const addresses = ['123 Oak St', '45 Oak St', '1234 Oak St', '5 Oak St'];
addresses.sort(collator.compare);
console.log(addresses);
// 输出: ['5 Oak St', '45 Oak St', '123 Oak St', '1234 Oak St']
排序器会识别每个字符串开头的数字序列,并以数字方式进行比较。它能够识别出 5 < 45 < 123 < 1234,即使按字典顺序比较会产生不同的顺序。
排序版本号
版本号是数字排序的一个常见用例。例如,软件版本 1.2.10 应排在 1.2.2 之后,但按字典顺序比较会产生错误的顺序。
const collator = new Intl.Collator('en-US', { numeric: true });
const versions = ['1.2.10', '1.2.2', '1.10.5', '1.2.5'];
versions.sort(collator.compare);
console.log(versions);
// 输出: ['1.2.2', '1.2.5', '1.2.10', '1.10.5']
排序器会正确比较每个数字组件。在序列 1.2.2、1.2.5、1.2.10 中,它识别出第三个组件按数字递增。在 1.10.5 中,它识别出第二个组件是 10,比 2 大。
处理产品代码和标识符
产品代码、发票号码和其他标识符通常混合了字母和数字。数字排序可以确保它们以逻辑顺序排序。
const collator = new Intl.Collator('en-US', { numeric: true });
const products = ['PROD-1', 'PROD-10', 'PROD-2', 'PROD-100'];
products.sort(collator.compare);
console.log(products);
// 输出: ['PROD-1', 'PROD-2', 'PROD-10', 'PROD-100']
字母前缀 PROD- 在所有字符串中都匹配,因此排序器会比较数字后缀。结果反映了数字递增的顺序,而不是字典顺序。
使用不同语言环境进行排序
numeric 选项适用于任何语言环境。尽管不同的语言环境可能对字母字符有不同的排序规则,但数字比较行为始终保持一致。
const enCollator = new Intl.Collator('en-US', { numeric: true });
const deCollator = new Intl.Collator('de-DE', { numeric: true });
const items = ['item1', 'item10', 'item2'];
console.log(items.sort(enCollator.compare));
// 输出: ['item1', 'item2', 'item10']
console.log(items.sort(deCollator.compare));
// 输出: ['item1', 'item2', 'item10']
由于字符串仅包含 ASCII 字符和数字,因此两种语言环境的结果相同。当字符串包含特定语言环境的字符时,字母比较会遵循语言环境规则,而数字比较仍然保持一致。
不进行排序的字符串比较
您可以直接使用 Collator 的 compare 方法来确定两个字符串之间的关系,而无需对整个数组进行排序。
const collator = new Intl.Collator('en-US', { numeric: true });
console.log(collator.compare('file2.txt', 'file10.txt'));
// 输出: -1 (负数表示第一个参数在第二个参数之前)
console.log(collator.compare('file10.txt', 'file2.txt'));
// 输出: 1 (正数表示第一个参数在第二个参数之后)
console.log(collator.compare('file2.txt', 'file2.txt'));
// 输出: 0 (零表示两个参数相等)
当您需要在不修改数组的情况下检查排序时,这非常有用,例如在插入一个项目到已排序列表中或检查一个值是否在某个范围内时。
理解小数比较的局限性
数字排序会比较数字序列,但不会将小数点视为数字的一部分。句点字符被视为分隔符,而不是小数分隔符。
const collator = new Intl.Collator('en-US', { numeric: true });
const measurements = ['0.5', '0.05', '0.005'];
measurements.sort(collator.compare);
console.log(measurements);
// 输出: ['0.005', '0.05', '0.5']
Collator 将每个测量值视为三个独立的数字部分:小数点前的部分、小数点本身以及小数点后的部分。它比较 0 和 0(相等),然后将小数点后的部分作为独立的数字进行比较:5、5 和 5(相等)。接着比较第二个小数位:无、5 和无。这会导致小数排序不正确。
要对小数进行排序,可以将它们转换为实际数字并按数值排序,或者使用字符串填充以确保正确的字典顺序。
将数字排序与其他选项结合使用
numeric 选项可以与其他排序选项(如 sensitivity 和 caseFirst)一起使用。您可以控制排序器如何处理大小写和重音,同时保持数字比较的行为。
const collator = new Intl.Collator('en-US', {
numeric: true,
sensitivity: 'base'
});
const items = ['Item1', 'item10', 'ITEM2'];
items.sort(collator.compare);
console.log(items);
// 输出: ['Item1', 'ITEM2', 'item10']
sensitivity: 'base' 选项使比较不区分大小写。排序器将 Item1、item1 和 ITEM1 视为等价,同时仍然正确比较数字部分。
重用排序器以提高性能
创建一个新的 Intl.Collator 实例需要加载区域设置数据并处理选项。当您需要对多个数组进行排序或执行多次比较时,可以创建一次排序器并重复使用。
const collator = new Intl.Collator('en-US', { numeric: true });
const files = ['file1.txt', 'file10.txt', 'file2.txt'];
const versions = ['1.2.10', '1.2.2', '1.10.5'];
const products = ['PROD-1', 'PROD-10', 'PROD-2'];
files.sort(collator.compare);
versions.sort(collator.compare);
products.sort(collator.compare);
这种方法比每次排序操作都创建一个新的排序器更高效。当需要对许多数组进行排序或频繁比较时,性能差异会变得显著。