1. 程式人生 > >Unicode 字串排序規則(一):如何確定單個字元的順序

Unicode 字串排序規則(一):如何確定單個字元的順序

一、一個具體的例子引發的問題

當今是國際化的時代,多種語言可能同時顯示在螢幕上。比如一個人可能喜歡聽華語歌、英文歌、韓文歌和日語歌,又比如他的聯絡人中有中國人、英國人、日本人、韓國人以及有英文名字的中國人。
在這種情況下,他的手機上需要維護一個列表,每一項可能是中文、韓文、英文和日文。在許多情況下需要對這個列表進行維護,那麼如何對這些表項進行排序呢?為了解決這個問題,至少要回答以下幾個問題:

  1. 中文、韓文、英文、日文的順序是怎樣的呢?那個語種在前面呢?
  2. 同一個語種內部如何排序呢?比如中文,是按照漢語拼音還是部首,抑或是筆畫數排序呢?
  3. 對於數字、標點符號、特殊符號等獨立於某一個書寫系統的字元 (character),如何處理呢?

二、事實上的排序標準: UCA+CLDR

使用這種方法來排序的公司和組織有 Apple、Google、IBM、MicroSoft、Amazon、Python、Wikimedia Foundation (Wikipedia)、Apache...
在蘋果的文件中,可以找到下面的描述:

Localized string comparisons are based on the Unicode Collation Algorithm, as tailored for different languages by CLDR (Common Locale Data Repository).

  1. UCA
    首先解釋一下 Collation 的含義。

    Collation is the general term for the process and function of determining the sorting order of strings of characters.

    如果有若干字串需要排序,確定排序的過程就是 Collation,可以認為是排序 (sort) 在字串領域的特化。
    Unicode Collation Algorithm (UCA) 是 Unicode 制定的如何比較兩個 Unicode 字串的規範。注意這裡是字串,而不僅僅是字元。
    UCA 的規則很複雜,我們以後再說。但從名字上可以看出,UCA 只是一個演算法,演算法需要資料才能產出結果。
    UCA 最後產出了一個文件,指定了預設情況下 Unicode 字元的順序。但是這僅僅是預設情況,也就是照顧了大多數情況(也就是排序對英語國家比較友好。。。)。對於其他地區的人們來說,就需要輸入和預設情況不同的資料,以獲得和當地習慣相符合的結果。比如同樣的漢字,在中國大陸是按照漢語拼音排序的,在香港就是按照筆畫數目排序的。

  2. CLDR
    Common Locale Data Repository (CLDR),從名字上可以看出,這個實際上是一堆資料的倉庫。對於指定的地區 (locale),可以從中找到指定的資料。再結合 UCA,就可以得到符合當期習慣的排序結果。

三、UCA 預設順序

UCA 最後產出了一個文件,在common/uca/allkeys_CLDR.txt。這個文件就指定了預設情況下的 Unicode 字元的順序。

    0031  ; [.1B3F.0020.0002] # DIGIT ONE          
    0661  ; [.1B3F.0020.0002] # ARABIC-INDIC DIGIT ONE

這個文件中的每一行都有上面的格式。分號;之前的部分是 Unicode 字元對應的 Unicode 碼點 (code point)。分號之後是用於 UCA 演算法的權重,用.分隔。#及之後部分是註釋。
所有的 Unicode 字元從上到下依次排序。

3.1 Unicode 字元分類

Unicode 把所有字元分為兩類,並按順序排列:

  1. common characters
    包括空格、標點、通用符號、貨幣符號和數字。
  2. script characters
    包括拉丁字母、希臘字母、漢字等。

把字元分類,便於把某一類字元統統放到另一類字元之前,比如把漢字放在英文之前。
注意:這裡預設排序並不是按照 Unicode 碼點順序依次排列!

3.2 利用 UCA 預設值排序的例子

rawArray = @[@"cc",@"曹操",@"bb",@"1",@"١",@"(en",@"(zh"];
NSArray *sortedArray = [rawArray sortedArrayUsingComparator:^NSComparisonResult(NSString *  _Nonnull obj1, NSString *  _Nonnull obj2) {
    return [obj1 compare:obj2 options:NSCaseInsensitiveSearch];
}];
__block NSMutableArray *codeUnits = [NSMutableArray array];
[sortedArray enumerateObjectsUsingBlock:^(NSString*  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    [codeUnits addObject:@([obj characterAtIndex:0])];
}];
NSLog(@"after default sort , result is %@, codeUnits is %@",sortedArray,codeUnits);

NSLocale *locale=[[NSLocale alloc] initWithLocaleIdentifier:@"en"];
sortedArray = [rawArray sortedArrayUsingComparator:^NSComparisonResult(NSString *  _Nonnull obj1, NSString *  _Nonnull obj2) {
    NSRange string1Range = NSMakeRange(0, [obj1 length]);
    return [obj1 compare:obj2 options:0 range:string1Range locale:locale];
}];
NSLog(@"after locale sort , result is %@",sortedArray);

輸出結果如下:

after default sort , result is ((en,1,bb,cc,١,曹操,(zh,), codeUnits is (40,49,98,99,1633,26361,65288,)  
after locale sort , result is ((en,(zh,1,١,bb,cc,曹操,)  

未指定地區資訊時,NSString的排序函式僅僅是按照UTF-16 code unit 的值排列的。在指定地區資訊是en後,按照allkeys_CLDR.txt中指定的順序,也更加符合人們的預期。依次是標點符號(英文和中文的左括號)、數字(阿拉伯數字 1 和另一種表示方法)、scripts(英文和中文)

四、CLDR 對排序結果進行調整 (tailor)

如上圖,CLDR 最新版本的資料有很多資料夾,都是用一種標記語言 (LDML) 來書寫的。我們一步一步來確定如何決定在中國大陸對 Unicode 字元進行排序。

4.1 確定採用何種排序規則

bcp47/collation.xml中,指出了可選的很多種排序方式,包括standardpinyinbig5hanstroke(筆畫排序)。

那麼在中國大陸應該採用哪種排序方法呢?可以在collation/zh.xml中找到指定的排序方式是pinyin。而繁體中文的預設排序方式是stroke

    <defaultCollation>pinyin</defaultCollation> in  collation/zh.xml  
    <defaultCollation>stroke</defaultCollation> in collation/zh_Hant.xml            

4.2 確定漢語內部的排序規則

collation/zh.xml 中可以看到下圖。和程式中的標頭檔案一樣,pinyin排序規則引入了private-pinyin這個規則,此外,還引入了預設的規則。即如果兩個字元(如阿拉伯數字 1 和 2 )在pinyin規則中找不到依據,那麼根據預設規則進行排序。這樣子做大大降低了維護成本,在原理上和 "Don't repeat yourself" 類似。
我們現在來看具體的排序規則。

  1. private-pinyin中,指定了可以標“聲調”的字母在各種聲調情況下的排列順序。
    你沒有看錯,mn也可以標聲調!
  2. 指定了拼音的順序以及同音字的順序
    首先按照拼音排序,表現為不同行之間的順序。對於同音字,也就是每一行之間的順序,先按照筆畫數排序,再按照kRSUnicode排序。
    請注意,在ā之前有一行,是用來構建索引的,即此行之下直至另一個索引都屬於A的索引之內。

4.3 不同語種字母的順序

當指定了地區之後,這個地區的字元將會在所有的 “script characters” 中排列第一。其他地區的字元按照預設順序排序。

  1. 確定當地的書寫系統 (scripts)
    ./main/zh_Hans_CN.xml中,看到<script type="Hans"/>
  2. 確定書寫系統包括哪些字元
    ./uca/FractionalUCA.txt中,搜尋first primary,得到下圖:
    在此行之下,即是所有漢字。第一個就是康熙字典的“一”,第二個是“㊀”。
  3. 其他書寫系統的字元的順序
    依據預設的順序,即在common/uca/allkeys_CLDR.txt規定的順序。

4.4 一個例子

對於以下的字串陣列排序:

    rawArray = @[@"上",@"㊤",@"μ",@"язык"];

這個字元陣列中,包括漢語、希臘語和俄語。指定不同的地區之後,得到不同的結果。

locale is el_CN, result is (µ,язык,上,㊤,)  //希臘語
locale is ru_CN, result is (язык,µ,上,㊤,) //俄語
locale is zh_CN, result is (上,㊤,µ,язык,) //中國
locale is en, result is (µ,язык,上,㊤,)        //英語

五、參考資料