- A+
转换字符的大小写是一个很常用的功能,例如 char.ToUpper
、char.ToLower
或者 TextInfo.ToUpper
和 TextInfo.ToLower
。这些 API 都只操作单个字符,如果遇到了一个字符范围,是需要遍历范围内的每个字符并依次执行转换。
以从大写字符转换为小写字符为例。对于一个字符范围 A-Z
,可以很容易的分辨出相应的小写字符范围为 a-z
,这里有几个条件:
- 从
A-Z
到a-z
的转换,可以通过字符值 + 32 计算得到。 A-Z
是连续的大写字符范围。
类似的,对于任意字符范围,只要确认其中连续的大写字符范围,再将其起始和结束字符转换为相应的小写字符,就完成了转换。
接下来,就是如何确认其中连续的大写字符范围了。系统正则表达式库中的一个内部类 RegexCharClass
提供了一个方案,这里定义了一个 LowerCaseMapping
数组,里面按顺序定义了所有大写字符范围和从大写字符到小写字符的转换方式。部分示例如下所示:
new LowerCaseMapping('u0041', 'u005A', LowercaseAdd, 32), new LowerCaseMapping('u00C0', 'u00DE', LowercaseAdd, 32), new LowerCaseMapping('u0100', 'u012E', LowercaseBor, 0), new LowerCaseMapping('u0130', 'u0130', LowercaseSet, 0x0069),
字符范围可以直接在这个数组内二分得到,比较特别的就是如何从大写字符转换到相应的小写字符。直接调用系统方法当然是一个方案,不过 RegexCharClass
的实现更为优化些。
根据统计,大小写字符间的关系只有两种:
- 将大写字符加上/减去一个字符得到相应的小写字符,例如
'A' + 32 = 'a'
。这种场景下大写字符和小写字符一般是互不覆盖的。 - 将大写字符加 1 得到相应的小写字符,例如
'Ā' + 1 = 'ā'
。这种场景下大写字符和小写字符会依次出现,基本处于同一个范围。
第一类关系很容易处理,LowercaseAdd
操作就是为当前字符加上一个固定的偏移量。只是会有一些特例,有时两个连续大写字符会对应同一个小写字符,例如 DŽ (u01C4)
和 Dž (u01C5)
对应的小写字符都是 dž (u01C6)
,此时使用 LowercaseSet
操作来直接设置为指定的字符。
第二类操作会略微复杂。如果将每个大写字符分别当作独立的字符范围,就会产生过多的零碎的字符范围,非常不利于性能优化。而如果将大写和小写字符混合为同一个范围,[Ā-ą]
,由于 ą + 1 = Ć
,这样的字符范围就会被转换为 [Ā-Ć]
从而导致错误。
这时就需要一种“对小写字符无效”的转换方式了,将大写字符按照奇偶分为两类:
- 如果大写字符为偶数(例如上面的
Ā (u0100)
),就选择LowercaseBor
操作,$lower = upper | 1$,这时大写字符(偶数)或 1 后会变成小写字符(奇数);而小写字符或 1 后仍然是原值。 - 如果大写字符为奇数(例如
Ĺ (u0139)
),就选择LowercaseBad
操作,$lower = upper + (upper & 1)$,这时大写字符(奇数)加上其最低位后会变成小写字符(偶数);而小写字符最低为是 0,加上之后仍然是原值。
RegexCharClass
只包含了将大写字符转换为小写字符的能力,需要自己扩展从小写字符转换为大写字符的操作。
第一类操作也是类似的,同样利用 Add
和 Set
就可以实现小写字符到大写字符的转换。第二类操作也同样需要区分奇偶,只是操作方式不太相同:
- 如果小写字符为奇数(例如上面的
ā (u0101)
),就使用 $upper = lower & 0xFFFE$,这时小写字符(奇数)的最后一位会被置 0,变成大写字符(偶数);而大写字符最后一位置 0 后仍然是原值。 - 如果小写字符为偶数(例如
ŀ (u0140)
),就使用 $upper = lower - (~lower & 1)$,这时小写字符(偶数)最低为取反后是 1,减去就会会变成大写字符(奇数);而大写字符最低为是 1,取反后仍然是 0,减去后会保持原值不变。
操作已经有了,就要计算所有小写字符的范围。这时我发现 RegexCharClass
内置的字符范围只包含了部分 InvariantCulture
中的小写字符,不太好确认正则表达式库是受限于历史问题还是使用了旧版 Unicode 标准。
同时,不同区域的大小写范围并不完全一致,要实现兼容其它区域性的范围大小写转换,就需要在运行时产出映射表,而非依靠提前计算。
我在这里实现了一个运行时计算方案,实测下来在 InvariantCulture 区域性下得到了 169 个大写字符范围和 189 个小写字符范围,在 zh-CN 区域性下得到了 170 个大写字符范围和 190 个小写字符范围。其中的差异就是在 zh-CN 区域性下,İ
会对应的小写字符 i
。
完整的字符范围大小写转换可以在这里找到。