Unicode 中的 BIDI 双向性算法

7,042 阅读7分钟
原文链接: blog.wfeng.me

一切得从复制 iOS 通讯录联系人手机号说起,有同学发现复制的号码是 “(415)555-3695”,长度应该是 13,但Debug 打印的长度却是 15,WTF?

通过断点发现是前后分别多了一个不知何用 Unicode 字符:

image-20180913152116622

U+202DU+202C 这两个是个啥?

其实这两个都是关于方向的 Unicode 控制字符,U+202D 简称 LROU+202C 简称 PDF 。那它们是做什么用,如何控制所谓方向的呢?

Unicode 的方向性

基础方向

定义的是一个区域的整体方向,例如一个页面、一个段落或一个句子。中英文环境一般是 (LTR) 从左至右,而阿拉伯文环境则为 (RTL) 右至左的书写顺序

字符方向

日常我们书写文字会知道,书写的方向是决定于所书写的文字,汉子、拉丁文字是从左至右,阿拉伯文、希伯来文则是从右至左。相应的,Unicode 字符在设计时就考虑了不同文字方向性的问题,因此定义了每个 Unicode 字符的方向属性。

每个 Unicode 编码都被赋予一个方向性,并且有强弱之分:

方向性 字符举例
Strong Left-to-Right (LTR) 强字符从左至右(英文字母、汉子都属于此类)
Strong Right-to-Left (RTL) 强字符从右至左(阿拉伯文字、希伯来文字属于此类)
Neutral 中性字符(大部分标点符号和空格属于此类)
Weak Left-to-Right (LTR) / Right-to-Left (RTL) 弱字符(数字和数字相关的符号属于此类)

Strong 强字符: 方向性确定,LTR 或 RTL,和上下文无关。并且可能会影响其前后字符的方向性。

Weak 弱字符: 和强字符一样方向性也是确定的,但是不会影响前后字符的方向性。

Neutral 中性字符: 方向性不确定,由上下文环境决定其方向。

Unicode 字符方向串 (Directional Run)

目前我们还没说到文章开始提到的 LROPDF 控制字符,下面我们先把这两个控制字符从号码中去掉,仅将 “(415)555-3695” 套用到阿拉伯文和中英文环境,观察会出现哪些问题:



هاتف: (415)555-3695


phone: (415)555-3695


座机: (415)555-3695

可以看到在中英文环境中,文本、数字和标点符号都按照从左至右的顺序书写,展示正常。

但在阿拉伯文环境中,电话号码好像按符号分割分组并方向展示了,这是怎么回事?

这里要引入 方向串 (Directional Run) 的概念,是指在一段文字中具有相同方向性的连续字符,并且其前后没有相同方向性的其它方向串。

全局方向、文本中的字符强弱类型 决定了如何分割方向串,以上面的例子做分析:

image-20180913174843950

文本被分为 6 个不同的方向串,问题显而易见,由于中性符号被全局方向影响,使得原本号码被拆分成不同方向串,被重新排序。

关于方向性的 Unicode 控制字符

为了解决上面的问题,Unicode 标准中定义了一系列方向性控制字符,这些字符在界面上不显示,也不占用任何展示空间。它们像是一些标记,影响着 BIDI 双向算法对文字书写方向的判断。

隐式双向控制字符 (Implicit Markers)

U+200E:   LEFT-TO-RIGHT MARK (LRM)
U+200F:   RIGHT-TO-LEFT MARK (RLM)

隐式控制字符的概念比较简单,可以理解为一个不会展示出来的强字符,LRM 为从左到右的强字符,而 RLM 为从右到左的强字符。

思考如何利用隐式控制解决上面号码的问题?

我们可以在每个中性字符 ‘-‘、’(‘、’’)’ 左右用 LTR 字符包裹,这样中性字符被左至右的强字符包裹,它的方向也应该会变为从左至右。

来吧,尝试一下 (阿拉伯文手机的 Unicode 编码为 U+0647 U+0627 U+0062a U+0641 ):

<!-- dir=rtl 设置 div 中的基础方向为从右至左 -->
<div dir=rtl>
    هاتف: ‎(‎415‎)‎555‎-‎3695
</div>


هاتف: ‎(‎415‎)‎555‎-‎3695

简直完美,成功了!

但写这么多未免繁琐,毕竟 iOS 实现相同效果只用了 LROPDF 两个字符,这两个字符又有什么作用呢?

显式双向控制字符 (Explicit Markers)

U+202A:   LEFT-TO-RIGHT EMBEDDING (LRE)
U+202B:   RIGHT-TO-LEFT EMBEDDING (RLE)
U+202D:   LEFT-TO-RIGHT OVERRIDE (LRO)
U+202E:   RIGHT-TO-LEFT OVERRIDE (RLO)
U+202C:   POP DIRECTIONAL FORMATTING (PDF)

显式控制字符需要成对使用,前四个字符 LER RLE LRO RLO 为开始字符,最后一个 PDF 为结束字符。

  • LRE & RLE : 接下来的文字片段内的方向变为 从左至右 / 从右至左。效果类似基础方向,将一段文本中的基础方向变更。
  • LRO & RLO : 顾名思义 override,接下来的所有 Unicode 字符的方向性将被覆盖为 从左至右强字符 / 从右至左强字符。

还以上面的通讯录文本为例:

<!-- 基础方向为从右至左 -->
<div dir=rtl>
    <!-- 使用 LRE 将号码部分文本方向改为从左至右 -->
    هاتف: ‪(415)555-3695‬
</div>


هاتف: ‪(415)555-3695‬

<!-- 基础方向为从右至左 -->
<div dir=rtl>
    <!-- 使用 LRO 将号码部分文本方向改为从左至右 -->
    هاتف: ‭(415)555-3695‬
</div>


هاتف: ‭(415)555-3695‬

Bingo!同样实现了通讯录所需效果。

那么,LRE / RLELRO / RLO 有什么区别,用在什么不同场景呢?接着看例子:

<!-- 基础方向为从左至右 -->
<div dir=ltr> 
    <div> here left to right, here right to left. </div>
    <div> here left to right‫, here right to left.‬ </div>
    <!-- 后半部分因为 RLE 使得文本方向改变 ',''.' 符号书写顺序变为从右至左 -->
</div>


here left to right, here right to left.
here left to right‫, here right to left.‬

<!-- 基础方向为从左至右 -->
<div dir=ltr>
    <div> here left to right, here right to left. </div>
    <div> here left to right‫, here right to left.‬ </div>
    <!-- 后半部分因为 RLO 使得字符方向属性被覆盖为强字符从右至左,英文字母也变成了从右至左书写 -->
</div>


here left to right, here right to left.
here left to right‮, here right to left.‬

iOS 对通讯录号码的处理

在从右至左的书写环境,虽然作为弱字符的数字还是按照从左至右的顺序书写,但是包含中性字符标点符号的电话号码,因为受到基础方向的影响,导致算法在不同环境下生成了不同的方向串,最终展示出错。

苹果为了避免这种错误产生,使用 LROPDF 控制字符包裹号码部分,使得其中的字符始终为强字符从左至右。

至此我们了解到,iOS 通讯录中复制电话号码都出的两个字符,并不是什么 bug,而是有意为之的,是为了避免不同语言环境下,电话号码的展示不一致。

总结

这里仅讨论了复制阿拉伯数字到输入框时可能遇到的坑。大家可知道阿拉伯语言环境下 iOS 通讯录中是不用 阿拉伯数字 的,用的是 阿拉伯文数字,类似中文 一、二、三 和 1、2、3 的区别

image-20180913200500301

img

如果中东用户复制阿拉伯文字数字到输入框,我们是自动转化为阿拉伯数字还是拒绝输入呢?

想做好全球化不容易啊~


相关文章

  1. Understanding Bidirectional (BIDI) Text in Unicode
  2. bidi 算法及 HTML 中的实现