MdxBuilder.exe 和的 mdict-utils 排序规则对照

整理了一份MdxBuilder.exe 和的 mdict-utils排序规则对照,供开发者们参考,如有错漏,实属正常。

1. 总体流程

项目 MdxBuilder mdict-utils
排序前处理 根据 Wide/Narrow 模式处理 Python str.lower()
Case 转换 自定义 MdxBuilderToLower 或 ASCII lower Python Unicode lower()
去标点 默认不去;stripKey=true 时去除非字母数字 固定去除 ASCII punctuation + ASCII space
排序单位 Wide:UTF-16 code unit;Narrow:byte Unicode code point
Locale 无 locale.strxfrm(),但默认 LC_COLLATE=C 时实际等同 identity
Stable sort 是 是
原始 Key 是否修改 否 否
Sort Key 是否写入 MDX 否 否

2. Case / 大小写处理

这是两者最重要的差异之一。

MdxBuilder Wide

MdxBuilderToLower()

它不是现代 Unicode lowercase,也不是 Unicode CaseFold。

一共验证了 827 个发生变化的 UTF-16 code points。

而且里面存在一些历史/实现上的特殊行为。

例如:

字符 MdxBuilder
A a
É é
Σ σ
ς σ
µ μ
× ÷
ſ s
ß ß
ẞ ẞ

其中最值得注意的是:

× U+00D7 → ÷ U+00F7

这是 MdxBuilder 中的 bug/quirk,已经验证过,不能“修正”。

mdict-utils

使用:

key.lower()

所以:

字符 Python lower()
A a
É é
Σ σ
ς ς
µ µ
× ×
ſ ſ
ß ß
ẞ ß

所以例如:

µ vs μ

MdxBuilder:

µ → μ
μ → μ

=> 两者相同

mdict-utils:

µ → µ
μ → μ

=> 两者不同

3. lower() vs CaseFold

这里特别容易混淆。

MdxBuilder

不是:

Unicode lower()

也不是:

Unicode casefold()

而是自己的历史 mapping。

mdict-utils

也是:

str.lower()

不是:

str.casefold()

所以:

ß

mdict-utils:

ß → ß

不会变成:

ss

而:

ẞ → ß

因此:

ẞ
ß

会产生相同 sort key。


4. Final Sigma

这是另一个非常重要的差异。

MdxBuilder

MdxBuilder 的自定义 mapping 中:

Σ → σ
ς → σ

因此:

Σ
σ
ς

最终会进入同一个 sort key。

mdict-utils

Python:

"Σ".lower() → "σ"
"σ".lower() → "σ"
"ς".lower() → "ς"

所以:

Σ == σ

但:

ς != σ

而且 Python lower() 对 Greek final sigma 存在上下文相关行为。

这也是确认:

MdictUtils 的 sort key 不具备严格的 prefix-preserving 特性。


5. 标点处理

这是另一个非常大的区别。

MdxBuilder

默认:

stripKey = false

时:

标点保留。

例如:

a-b
a b
a_b
a$b

sort key 中仍然存在:

a-b
a b
a_b
a$b

如果:

stripKey = true

才进行过滤。


mdict-utils

MDX 路径固定执行:

string.punctuation + " "

也就是:

!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

再加:

ASCII space U+0020

因此:

a-b  → ab
a b  → ab
a_b  → ab
a$b  → ab
(a)b → ab

所以:

a-b
ab
a b
a_b

在 mdict-utils 中可能产生相同 sort key。


6. Unicode 标点

这里很容易误以为 mdict-utils 会删除“所有标点”。

实际上不是。

它只删除 Python:

string.punctuation

也就是 ASCII 32 个 punctuation。

因此:

中,文
中。文
中、文
em—dash
«x»
‘x’

这些 Unicode punctuation 都会保留。

同样:

×
÷

也不会被删除。


7. 空格和 whitespace

MdxBuilder

默认情况下空格可以参与排序。

mdict-utils

只有:

U+0020 ASCII space

会被删除。

例如:

"a b" → "ab"

但:

"a\tb"
"a b"       NBSP
"a b"      U+3000

不会因为 punctuation stripping 而被删除。

另外,pack_mdx_txt() 在解析 TXT 时还有一个不同的行为:

line.strip()

它会先去除 ASCII whitespace 两端。

所以这里要区分:

TXT parsing
vs
sorting

这两个阶段不是一回事。


8. Unicode accent / diacritic

这是你最近发现那个问题的关键。

MdxBuilder

不会自动进行 accent stripping。

例如:

e
é

通常仍然是不同 sort key。

mdict-utils

也不会:

é → e

它只是:

lower()
+
ASCII punctuation stripping

因此:

e = U+0065
é = U+00E9

排序时:

e < é

所以正常的 mdict-utils 结果应该是:

delegue...
délégue...

两个 block 分开。

你实际看到的:

delegue
délégué
délégue
...

不是 mdict-utils comparator 的行为,而更像阅读器自己的 accent-insensitive index/sort。你上传的调查报告也通过实际 MDX 构建验证了这一点。


9. 排序单位

这是 MdxBuilder 和 mdict-utils 结构上最大的区别之一。

MdxBuilder Wide

比较:

unsigned UTF-16 code unit

例如 supplementary character:

𐐀 = U+10400

UTF-16:

D801 DC00

所以 MdxBuilder 实际比较的是:

D801 ...

而不是:

00010400

mdict-utils

Python str 比较的是:

Unicode code point

因此:

𐐀 = U+10400

就是直接按照:

0x10400

比较。

这会导致 supplementary characters 在两个系统中的排序不同。

例如:

A = U+FF21
𐐀 = U+10400

Unicode code point:

FF21 < 10400

所以 mdict-utils:

A < 𐐀

但是 MdxBuilder UTF-16:

A = FF21
𐐀 = D801 DC00

所以:

𐐀 < A

这不是 bug,而是两个实现定义不同。


10. Narrow MdxBuilder 更特殊

MdxBuilder 还有一个 Narrow pipeline。

它不是 Wide 的 Unicode 排序。

ASCII

只做:

A-Z → a-z

Big5

识别 Big5 lead/trail,然后保留原始双字节。

GBK

根据原始 byte pair 判断。

最后:

unsigned byte comparison

所以:

MdxBuilder Wide
    → UTF-16 code units

MdxBuilder Narrow
    → raw bytes

mdict-utils
    → Unicode code points

三个完全不同的排序单位。


11. GBK / Big5

MdxBuilder Narrow

如果输入是 GBK / Big5:

decode → ❌

不是先转换成 Unicode 再排序。

而是直接按照:

encoded bytes

排序。

mdict-utils

相反。

它先:

bytes
 ↓
Python str
 ↓
lower()
 ↓
sort
 ↓
encode GBK/Big5

所以即使最终 MDX 是 GBK:

排序仍然是 Unicode code-point ordering。

Claude 实际测试 UTF-8、UTF-16、GBK 三种输出,排序结果相同,也验证了这一点。


12. Locale

MdxBuilder

没有 locale collation。

就是直接:

unsigned UTF-16 / byte comparison

mdict-utils

代码中有:

locale.strxfrm()

但当前实际环境:

LC_COLLATE = C

所以:

strxfrm(s) == s

实际效果仍然是:

Unicode code-point comparison

而且 mdict-utils 自己没有调用:

setlocale()

所以实现 native backend 时,不应该真的调用 std::locale 或 strxfrm,否则反而可能引入 Android device locale 依赖。


13. Tie / 相同 sort key

两者都是:

stable sorting。

MdxBuilder

使用 stable sort。

如果:

key1 == key2

保持 source order。

mdict-utils

Python:

list.sort()

是 stable Timsort。

例如:

deleguee
deleguee
delegue, e

因为:

delegue, e
    ↓ remove punctuation
deleguee

所以三个 sort key 相同。

最终顺序取决于 source order。Claude 的实际 MDX 验证也确认了这一点。


14. MdictUtils 一个“看起来有 tie-break,实际上没有”的地方

原始代码还有:

if len(key1) > len(key2):
    return -1
elif len(key1) < len(key2):
    return 1

以及后面的:

rstrip(string.punctuation)

但这些代码实际上是 unreachable。

因为前面已经:

if key1 > key2:
    return 1
elif key1 < key2:
    return -1

如果两个 Python strings 既不大于也不小于,那么它们就是完全相等。

因此:

len(key1) == len(key2)

后面的 rstrip 也不会产生区别。

所以最终就是:

sort key equal
    ↓
return 0
    ↓
stable sort

报告中的 200,000 次 instrumented comparison 也验证了这些 branch 从未触发。


15. Original Key / 存储内容

这一点两者其实相同:

排序 key ≠ 存储 key。

MdxBuilder

例如:

Apple

可能生成:

apple

作为 sort key。

但最终 MDX 保存:

Apple

mdict-utils

例如:

a-b

sort key:

ab

但最终 MDX 保存:

a-b

所以:

Original Key
    ├──→ Sort Key → sorting
    │
    └──→ Stored Key → MDX

16. 最终完整对照

排序规则 MdxBuilder Wide MdxBuilder Narrow MdictUtils
Case conversion 自定义 MdxBuilderToLower ASCII A-Z → a-z Python str.lower()
Unicode CaseFold :cross_mark: :cross_mark: :cross_mark:
ß → ss :cross_mark: :cross_mark: :cross_mark:
µ → μ :white_check_mark: :cross_mark: :cross_mark:
ς → σ :white_check_mark: :cross_mark: :cross_mark:
ſ → s :white_check_mark: :cross_mark: :cross_mark:
ẞ → ß :cross_mark: :cross_mark: :white_check_mark:
× → ÷ :white_check_mark: 特殊 bug :cross_mark: :cross_mark:
ASCII punctuation 默认保留 默认保留 删除
ASCII space 默认保留 默认保留 删除
Unicode punctuation 默认保留 默认保留 保留
Accent stripping :cross_mark: :cross_mark: :cross_mark:
Locale collation :cross_mark: :cross_mark: 实际 C → identity
Sort unit UTF-16 code unit byte Unicode code point
Supplementary characters surrogate units raw bytes code points
GBK/Big5 Wide decode / Narrow raw bytes raw bytes Unicode
UTF-8 Wide 不属于 Narrow Unicode
Tie-break stable stable stable
Equal key source order source order source order
Original key modified? :cross_mark: :cross_mark: :cross_mark:
Sort key stored? :cross_mark: :cross_mark: :cross_mark:
stripKey 有 有 没有,忽略该参数
Prefix preserving 原有行为 原有行为 不保证
External dependency 无 无 无

17. 最容易混淆的几个例子

µ vs μ

MdxBuilder:
µ → μ
μ → μ
=> equal

MdictUtils:
µ → µ
μ → μ
=> different

ς vs σ

MdxBuilder:
ς → σ
σ → σ
=> equal

MdictUtils:
ς → ς
σ → σ
=> different

ß vs ss

MdxBuilder:
ß → ß
ss → ss
=> different

MdictUtils:
ß → ß
ss → ss
=> different

两者都不是 CaseFold。

a-b vs ab

MdxBuilder default:
a-b
ab
=> different

MdictUtils:
a-b → ab
ab  → ab
=> equal

delegue vs délégue

MdxBuilder:
e / é
=> different

MdictUtils:
e / é
=> different

两者都不会把 é 自动变成 e。

因此:

delegue...
délégue...

应该是两个连续 block。

之前看到的:

delegue
délégué
délégue
...

不是 MdictUtils 的排序规则。 这点已经通过真实 Python + 真实 MDX 双重验证。


10 个赞