侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 270 篇文章
  • 累计收到 7 条评论

中文姓氏按拼音排,章节数字仍按字符排

2026-11-14 / 0 评论 / 16 阅读

中文姓氏按拼音排,章节数字仍按字符排

把一批中文姓名交给默认排序,陈六落在最后;给比较器指定中文区域后,它到了最前面。可同一把中文排序器遇到「第10章」和「第2章」,10 仍排在 2 前。区域规则解决了汉字怎么比较,并不会顺手把字符串里的数字变成数值。

这组结果来自 Node.js v26.8.1,运行时使用 full-icu,默认 locale 是 en-US,系统 locale 相关环境变量未设置。以下排序与耗时都对应 tools/verify-intl-collator.mjs 的本机输出,不是对所有运行时的保证。

姓名顺序先看比较规则

脚本把同一批 15 个姓名分别交给六种比较器。默认比较与不带参数的 localeCompare() 输出完全相同;中文 locale 把陈、何、李、吕排到前段;笔画排序又是另一份名单。

比较方式 实测开头 实测结尾
默认比较 何四 / 吕五 / 吴十 郑一 / 陈六
localeCompare('zh') 陈六 / 何四 / 李四 赵七 / 郑一 / 周九
Collator('zh-Hans-CN') 陈六 / 何四 / 李四 赵七 / 郑一 / 周九
Collator('zh-Hans-CN-u-co-stroke') 王五 / 吕五 / 孙八 赵七 / 郑一 / 秦二
Collator('en') 何四 / 吕五 / 吴十 郑一 / 陈六
node 版本: v26.8.1
ICU: full-icu
默认 locale: en-US
Collator('zh-Hans-CN'): 陈六 何四 李四 吕五 欧阳修 秦二 施六 孙八 王五 吴十 许三 张三 赵七 郑一 周九
Collator('zh-Hans-CN-u-co-stroke'): 王五 吕五 孙八 许三 何四 吴十 张三 李四 周九 欧阳修 陈六 施六 赵七 郑一 秦二

zh 和 zh-Hans-CN 在这批数据上给出相同顺序;追加 u-co-stroke 后则变成笔画序。这里不能把「中文排序」理解成唯一固定的名单:排序器采用哪个 locale 和 collation,直接改变比较结果。默认环境恰好是 en-US 时,不带参数的比较也不会替应用猜测它想要中文拼音顺序。

const collator = new Intl.Collator('zh-Hans-CN');
const sortedNames = names.toSorted(collator.compare);

比较器适合按用途建一次再复用。实际项目的姓名顺序还受产品选定的排序约定影响;如果目标是按笔画,而代码写的是默认中文 collation,输出虽稳定,也可能不是产品要的规则。

中文 locale 不会开启数字排序

章节号的对照更直观:

比较方式 输出顺序
输入 第2章 / 第10章 / 第1章 / 第20章 / 第3章
默认比较 第10章 / 第1章 / 第20章 / 第2章 / 第3章
Collator('zh-Hans-CN') 第10章 / 第1章 / 第20章 / 第2章 / 第3章
加 numeric: true 第1章 / 第2章 / 第3章 / 第10章 / 第20章
默认比较: 第10章 第1章 第20章 第2章 第3章
Collator('zh-Hans-CN'): 第10章 第1章 第20章 第2章 第3章
Collator('zh-Hans-CN', {numeric:true}): 第1章 第2章 第3章 第10章 第20章

只指定 locale,默认结果仍是按字符串片段比较,所以第10章会跑到第2章前。要让连续数字作为数字比较,需要明确打开选项:

const chapterCollator = new Intl.Collator('zh-Hans-CN', {
  numeric: true,
});

const chapters = ['第2章', '第10章', '第1章', '第20章', '第3章'];
console.log(chapters.toSorted(chapterCollator.compare));
// [ '第1章', '第2章', '第3章', '第10章', '第20章' ]

这也是最容易漏掉的配置差异:locale 决定语言相关的比较规则,numeric 则决定数字子串是否按数值顺序处理。文件名或章节号中出现数字片段时,单写 new Intl.Collator('zh-Hans-CN') 并不等于自然排序。反过来,编号如果包含业务定义的前导零、特殊分隔符或非十进制规则,也不能只凭 numeric 推断整个业务排序已经正确。比如「第02章」与「第2章」究竟要不要视作相邻甚至相等,是产品对编号的定义,不应由排序器替产品做决定。数据里若混入空字符串或缺失字段,也应在比较之前决定如何处理;让排序器直接比较未规范化的业务值,只会把未定义的次序藏进列表。

灵敏度不会把不同字变成相等

脚本还比较了「妈、麻、马、骂」。默认字符串比较的结果是「妈、马、骂、麻」,Collator('zh') 则是「妈、麻、马、骂」。设为 sensitivity: 'base' 后,本次输出仍是后一个顺序;'妈'.localeCompare('麻', 'zh', { sensitivity: 'base' }) 的结果仍为 -1。

因此,不能只看到 base 就断定声调差异已被抹平,或同音字会相等。排序器返回负数、零、正数表达的是先后关系,不是拼音键,也不意味着字符串被归一化。若业务要把同音姓名放进同一组,或要稳定地按自定义拼音键排序,需要另行生成业务排序键;不能把比较结果当成键值存起来。

localeCompare() 和 Intl.Collator 在这里都是为排序提供比较结果。需要大量比较时,复用 Intl.Collator 可以避免在比较回调里反复构造实例。脚本对 5 万个三字字符串排了一遍,每种写法各测三次:每个元素新建 Collator 是 1113、1083、1099 毫秒;无参数 localeCompare 是 22 毫秒;复用同一 Collator 是 18 毫秒。另一次 2 万条测试分别耗时 9 毫秒和 7 毫秒,日志给出的比值为 1.28 倍。日志中的原始计时如下:

每个元素 new Collator: 1113ms / 1083ms / 1099ms
localeCompare 无参数: 22ms / 22ms / 22ms
复用同一个 Collator: 18ms / 18ms / 18ms
localeCompare 无参数: 9ms
复用 Collator: 7ms
倍数: 1.28x

这只是这台机器上这份脚本的耗时,不是性能承诺;不能据此说所有数据集、引擎或 locale 都会有相同比值。我会把排序器放在重复调用的比较路径之外,主要是避免不必要的实例创建,是否有明显收益仍应按实际数据量测量。

姓名列表要中文顺序时,明确指定目标 locale,并确认产品采用拼音还是笔画等规则;章节号要自然数字顺序时,再设置 numeric: true。如果比较规则与业务要求一致,复用一个 Collator 就够了;若顺序涉及自定义优先级或特殊编号,先定义明确的排序键,不要指望 locale 自动理解业务含义。

    🤞 分享