算法与数据结构 / regex · Unicode 字符模型与一个自研引擎 / 排序:为什么 sort 把 Z 排在 a 前 待审核 22 / 36
字符的真面目 · 排序不在码点里

排序:为什么 sort 把 Z 排在 a 前

["Z","a"].sort() 得到的还是 ["Z","a"]:默认的 sort() 逐个比较 UTF-16 码元值,而大写字母的码点全在小写之前,带音标的字母又被甩到 ASCII 之后,"file10" 还会排在 "file9" 前面。按某种语言的字典序排是另一回事,由 Unicode Collation Algorithm 定义,JS 里走 Intl.Collator。与 拼音 同理:排序规则不写在码点里,是另一份按 locale 调的权重表。

1 · 码元序不是字母序

把同一组词分别交给 sort()Intl.Collator 排,差别一眼可见:码元序里大写聚一堆、小写聚一堆、带音标的垫底,不是任何人读得下去的顺序。中文传 'zh' 会按拼音排,这也印证了排序规则在码点之外。

图 1-1 · 同一组词在码元序与字典序下的排列对照,位置不同的行高亮。可改词表或切换 locale。

2 · 权重的三层

UCA 给每个字符算多级权重:primary 管基字母(ab),secondary 管变音(aá),tertiary 管大小写(aA)。Intl.Collatorsensitivity 就是「比到第几层」,比得越浅,越多字符被视作相等。'base'aáaA 都相等,'accent' 下音标开始区分而大小写仍不分,'variant'(默认)三层全比。

图 2-1 · 五组字符对在四档 sensitivity 下的比较结果,判为相等的格子高亮。

注 · 不分大小写与音标的搜索用 sensitivity: 'base',区分音标但不分大小写用 'accent',完整排序用默认的 'variant'。注意 compare(a, b) === 0 只表示「排序意义上相等」,不等于 ===;若还担心同字多形,比较前先 归一化

3 · locale 的差别

同一组字母,不同语言排出的顺序不同。实测 a z ä ö å 这组:英语与德语都排成 a å ä ö z——它们把音标当次级差异,ä 紧挨着 a;瑞典语排成 a z å ä ö,把 å ä ö 当独立字母放到字母表末尾。同一份数据、同一个 API,只因 locale 不同就给出两种顺序。

图 3-1 · 同一组字母在英语、德语与瑞典语下的排序结果对照。

4 · 数字的比法

字符串里的数字默认按字符比,'1' 小于 '9',于是 "file10" 排在 "file9" 前面。Intl.Collatornumeric: true 会把连续数字当数值比,这正是文件管理器里「自然排序」的做法。

图 4-1 · 一组带序号的文件名在默认与 numeric 模式下的排序对照,位置不同的行高亮。

建议 · 凡是给人看的排序,或大小写与音标无关的查找,都别用裸 sort()<,那是码元序。用 Intl.Collator(locale, { sensitivity, numeric }),把「按哪种语言、忽略到哪层、数字怎么比」交给标准。中文传 'zh' 即按拼音排。

5 · 参考文献

  1. Unicode. UTS #10: Unicode Collation Algorithm. 多级权重的定义、默认排序元素表与各级比较规则。unicode.org
  2. Unicode. CLDR Collation Charts. 各 locale 对默认权重表的定制排出的实际字母序,瑞典语把 å ä ö 放到字母表末尾即见于此(规则源文件为 CLDR 的 common/collation/sv.xml)。unicode.org
  3. Ecma International. ECMA-402: Intl.Collator. sensitivitynumericcaseFirst 等选项的语义。tc39.es
  4. MDN. Intl.Collator. 常见用法与 compare 作为 sort 比较器的写法。developer.mozilla.org