深入理解蘋果系統(Unicode)字串的排序方法

騰訊雲加社群發表於2018-11-20

歡迎大家前往騰訊雲+社群,獲取更多騰訊海量技術實踐乾貨哦~

本文由iminder發表於雲+社群專欄

Unicode編碼

我們知道計算機是不能直接處理文字的,而是和數字打交道。因此,為了表示文字,就建立了一個字元到數字的對映表,叫做編碼。最著名的字元編碼就是ASCII了,它使用7-bit來表示應用字母表以及數字和其他字元。這對於英語來說是夠用了,但是對於其他語言,這個7-bit就不能滿足條件了,因為字元遠遠超過了7-bit所能表示的最大個數。因此1987年,來自幾個大的科技公司的工程師開始合作開發一種致力於能在全世界的所有書寫系統中都能通用的字元編碼系統,並與1991年10釋出了Unicode的1.0.0標準。2018年6月釋出了Unicode的11.0版本。這裡就不再對Unicode做過多的介紹,值得注意的是,在iOS開發中,常使用的的NSString是基於Unicode-16來開發的,這是因為當時開發這個的時候Unicode標準還是以16bit固定長度來編碼,這就導致使用上的一些坑,建議大家閱讀下這篇文章:NSString and Unicode

#UCA和CLDR:最常用到的排序標準

介紹完Unicode編碼之後,我們就可以來介紹UCA(Unicode Collation Algorithm)和CLDR(Common Locale Data Repository)了,因為蘋果的NSString的介紹文件裡有這麼一句話:

Localized string comparisons are based on the Unicode Collation Algorithm, as tailored for different languages by CLDR (Common Locale Data Repository). Both are projects of the Unicode Consortium. Unicode is a registered trademark of Unicode, Inc.
複製程式碼

說白了,蘋果系統的NSString字串排序是基於UCA的,並且在不同語言下,經過CLDR來裁剪的。

UCA(Unicode Collation Algorithm)

UCA的介紹官方文件介紹在這裡:UCA介紹。其中第一句話就寫的很清楚,

Collation is the general term for the process and function of determining the sorting order of strings of characters.
複製程式碼

對字串排序的過程就是Collation,UCA就是Unicode表示的字串進行排序的規則,制定這個規則的原因是不同語種對字串的排序規則要求是不一樣的,比如,德國、法國和瑞士對相同的字元排序的規則是不一樣的,甚至在同一個語言下比如中文,多音字這種在不同組合裡,排序的先後順序也是不一樣的。

img
差異化舉例

因此可以想象,UCA指定的規則比較複雜。感興趣的可以讀下前面貼的UCA介紹,裡面有具體的排序規則介紹。

CLDR(Common Locale Data Repository)

CLDR的官方文件在這裡:CLDR介紹。CLDR是一堆語言資料倉儲,為軟體提供各種世界語言版本提供了基礎,目前在使用CLDR的公司有:

Apple (macOS, iOS, watchOS, tvOS, and several applications; Apple Mobile Device Support and iTunes for Windows; …) Google (Web Search, Chrome, Android, Adwords, Google+, Google Maps, Blogger, Google Analytics, …) IBM (DB2, Lotus, Websphere, Tivoli, Rational, AIX, i/OS, z/OS,…) Microsoft (Windows, Office, Visual Studio, …)

其他公司:

ABAS Software, Adobe, Amazon (Kindle), Amdocs, Apache, Appian, Argonne National Laboratory, Avaya, Babel (Pocoo library), BAE Systems Geospatial eXploitation Products, BEA, BluePhoenix Solutions, BMC Software, Boost, BroadJump, Business Objects, caris, CERN, Debian Linux, Dell, Eclipse, eBay, EMC Corporation, ESRI, Firebird RDBMS, FreeBSD, Gentoo Linux, GroundWork Open Source, GTK+, Harman/Becker Automotive Systems GmbH, HP, Hyperion, Inktomi, Innodata Isogen, Informatica, Intel, Interlogics, IONA, IXOS, Jikes, jQuery, Library of Congress, Mathworks, Mozilla, Netezza, OpenOffice, Oracle (Solaris, Java), Lawson Software, Leica Geosystems GIS & Mapping LLC, Mandrake Linux, OCLC, Perl, Progress Software, Python, QNX, Rogue Wave, SAP, Shutterstock, SIL, SPSS, Software AG, SuSE, Symantec, Teradata (NCR), ToolAware, Trend Micro, Twitter, Virage, webMethods, Wikimedia Foundation (Wikipedia), Wine, WMS Gaming, XyEnterprise, Yahoo!, Yelp

對於不同區域(local),可以找到不同的資料CLDR,結合UCA對字串進行排序,就做到了不同語言下的本地化排序。可以去 cldr.unicode.org/ 下載最新的CLDR庫,後面將會用到裡面的一些內容。

字元分類與排序規則

字元分類與Unicode碼點值排序

Unicode把所有的字元分為兩類:

  1. common charaters 包括空格,標點,通用符號,貨幣符號,數字等。
  2. script charaters 包括拉丁字母,希臘字母,漢字等。 這樣經過分類,便於把一類字元統一集中在一起。

通常情況下,我們是通過unicode 的UTF-16碼點值逐個進行比較大小的來進行排序的。

NSArray *rawArray = @[@"愛你", @"一生一世",@"㊀", @"上",@"㊤",@"μ",@"язык",@"..",@"123",@"@",@"AA",@"abc",@"abb"];
//1. 預設排序方式
NSArray *defaultedSortedArray = [rawArray sortedArrayUsingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
    return [obj1 compare:obj2 options:NSCaseInsensitiveSearch];
}];
__block NSMutableArray *codeUnits = [NSMutableArray array];
[defaultedSortedArray enumerateObjectsUsingBlock:^(NSString*  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    [codeUnits addObject:@([obj characterAtIndex:0])];
}];
NSLog(@"預設Unicode碼點值排序 %@ 對應的各個字串的首字元碼點值是 %@", [defaultedSortedArray descriptionWithLocale:cnLocal], codeUnits);
複製程式碼

輸出結果是

排序結果 
 ..  123  @  AA  abb  abc  μ  язык  ㊀  ㊤  一生一世  上  愛你 
 對應的各個字串的首字元碼點值是 
 46  49  64  65  97  97  956  1103  12928  12964  19968  19978  29233 
複製程式碼

我們常用的各種字元的碼點值範圍是:

  • 0-9 U+0030 - U0039
  • a-z U+0061 - U+007A
  • A-Z U+0041 - U+005A 具體可通過:unicode-table查詢。

UCA 預設排序

在我們前面下載的檔案CLDR庫有個/common/uca/allkeys_CLDR.txt檔案,它表示我們指定locale為“en”或者說是預設的排序規則。它的格式是

0000  ; [.0000.0000.0000] # <NULL>
0001  ; [.0000.0000.0000] # <START OF HEADING>
0002  ; [.0000.0000.0000] # <START OF TEXT>
0003  ; [.0000.0000.0000] # <END OF TEXT>
複製程式碼

分號前的值表示碼點,分號後中括號裡面的值表示UCA演算法權重,用.號來區分,Unicode字元就是按照這個規則從上到下排序。

NSLocale *enLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en"];
defaultedSortedArray = [rawArray sortedArrayUsingComparator:^NSComparisonResult(NSString*  _Nonnull obj1, NSString*  _Nonnull obj2) {
    return [obj1 compare:obj2 options:0 range:NSMakeRange(0, obj1.length) locale:enLocale];
}];
NSLog(@"預設排序規則或者指定地區為locale後的排序結果是 %@", [defaultedSortedArray descriptionWithLocale:cnLocal]);
複製程式碼

排序結果是

預設排序規則或者指定地區為en後的排序結果是 
 ..  (ch  (en  @  123  AA  abb  abc  μ  язык  ㊀  一生一世  上  ㊤  愛你 
複製程式碼

這種排序依次為符號,數字,英文/漢字等script charaters。

CLDR調整後的排序

在下載的CLDR檔案中,有個common/bcp47/collation.xml檔案,列出了可選的排序方式,有standard,pinyin, stroke(筆畫排序)等。

img
排序可選方式

那如何確定各個區域語言下,該使用哪種排序規則呢,我們可以看到common/collation/資料夾下,有很多標記語言LDML檔案,這些檔案就是表示在不同區域語言下,採用的排序規則。

img

我們開啟zh.xml,這個就是我們簡體中文的排序規則,可以看到,裡面預設採用的排序是pinyin排序,並且在開頭還寫了各個聲調字母的排序先後順序。

img

  1. 首先按照pinyin聲調的先後順序進行排序,即zh.xml底下列出的先後順序進行排序。
  2. 如果是在同一行的漢字,則按照筆畫由少到多的順序進行排序。
  3. 如果還不能區分大小,就按照kRSUnicode (偏旁索引的方式,按照康熙字典的定義)的先後順序進行排序。

假如我們指定區域為zh_CN,則對於字串中出現的中文則排在其他語言字串前面。其他script charater則按照allkeys_CLDR.txt的順序進行進行排序。值得注意的是,中文由於多音字,在這裡不一定能夠完全按照我們的習慣排序正確,比如“重逢(chong feng)”就沒有第一個拼音chong去排,而是按照zhong來排列的。

預設排序規則或者指定地區為zh_CN後的排序結果是 
 ..  (ch  (en  @  0124  123  艾你  愛你  産  上  ㊤  ㊀  一生一世  重逢  重要  aa  AA  abb  μ  язык 
 
 預設排序規則或者指定地區為ru_CN後的排序結果是 
 ..  (ch  (en  @  0124  123  язык  aa  AA  abb  μ  ㊀  一生一世  上  ㊤  愛你  産  艾你  重要  重逢 
 
複製程式碼

至此,我們大致講清楚了幾種排序規則。

蘋果系統的排序

前面我們已經說了,蘋果系統的NSString排序是UCA和CLDR規則的。NSString提供了很多的排序方法,但最終,所有的都是呼叫了compare:options:range:locale:來進行處理,只是傳入的引數不同。可以在NSString.swift 中檢視具體的實現。這麼多排序方法中,其中之一是localizedStandardCompare:, 這個方法是蘋果系統推薦的,在給使用者展示的列表資料的名字或者其他字串進行排序時所使用的方法。我們看到,它的內部實現是

   public func localizedStandardCompare(_ string: String) -> ComparisonResult {
        return compare(string, options: [.caseInsensitive, .numeric, .widthInsensitive, .forcedOrdering], range: NSRange(location: 0, length: length), locale: Locale.current._bridgeToObjectiveC())
    }
複製程式碼

其中用到的四個Options引數是

NSCaseInsensitiveSearch  //大小寫不敏感
NSNumericSearch //對字串中出現的數字字元進行數字化的大小比較,比如Foo2.txt < Foo7.txt < Foo25.txt
NSWidthInsensitiveSearch //忽略寬度,按照實際表示的意思來對比,如'a' = UFF41
NSForcedOrderingSearch //強制返回Ascending或者Descending,和NSCaseInsensitiveSearch結合起來就是例如"aaa" > "AAA"
複製程式碼

並且指定了當前的區域locale作為引數,這就相當於指定使用CLDR進行排序,如果是在手機上,這個方法的呼叫和系統當前的區域設定是有很大關係的,這和我們程式碼中設定locale是一個道理。我們可以這樣理解,呼叫這個方法得到的結果和在iOS Files中檔名選擇按照名稱排序得到的結果是一樣的。在iOS中,當我們的區域設定為中國時,排序順序就是 標點符號等特殊符號>數字>中文>英文等其他

img
區域設定成中文後的排序

自此,對localizedStandardCompare:的使用,大家應該比較清楚了。

數字的比較

這裡單獨把數字字串的比較列出來,是因為一些人對這裡比較迷惑。由於localizedStandardCompare:中有使用NSNumericSearch選項,這裡簡單來說,就是假如目前兩個字串是相等的,兩者都出現了數字,則分別從兩者種取出這段數字進行數字化來比較大小,按照數字大小排序。為了驗證這裡的邏輯,我看了下CFString.cCFStringCompareWithOptionsAndLocale這個方法的實現,這個就是compare實際呼叫的的比較方法。其中關於數字大小比較的程式碼如下:

if (numerically && ((0 == strBuf1Len) && (str1Char <= '9') && (str1Char >= '0')) && ((0 == strBuf2Len) && (str2Char <= '9') && (str2Char >= '0'))) { // If both are not ASCII digits, then don't do numerical comparison here
        uint64_t intValue1 = 0, intValue2 = 0;	// !!! Doesn't work if numbers are > max uint64_t
        CFIndex str1NumRangeIndex = str1Index;
        CFIndex str2NumRangeIndex = str2Index;

        do {
            intValue1 = (intValue1 * 10) + (str1Char - '0');
            str1Char = CFStringGetCharacterFromInlineBuffer(&inlineBuf1, ++str1Index);
        } while ((str1Char <= '9') && (str1Char >= '0'));

        do {
            intValue2 = intValue2 * 10 + (str2Char - '0');
            str2Char = CFStringGetCharacterFromInlineBuffer(&inlineBuf2, ++str2Index);
        } while ((str2Char <= '9') && (str2Char >= '0'));

        if (intValue1 == intValue2) {
            if (forceOrdering && (kCFCompareEqualTo == compareResult) && ((str1Index - str1NumRangeIndex) != (str2Index - str2NumRangeIndex))) {
                compareResult = (((str1Index - str1NumRangeIndex) < (str2Index - str2NumRangeIndex)) ? kCFCompareLessThan : kCFCompareGreaterThan);
                numericEquivalence = true;
                forcedIndex1 = str1NumRangeIndex;
                forcedIndex2 = str2NumRangeIndex;
            }

            continue;
        } else if (intValue1 < intValue2) {
            if (freeLocale && locale) {
                CFRelease(locale);
            }
                return kCFCompareLessThan;
            } else {
                if (freeLocale && locale) {
                    CFRelease(locale);
                }
                return kCFCompareGreaterThan;
        }
    }
複製程式碼

這段程式碼的含義就是,如果兩個字串都是以數字開始(也可能是字串前面都相等,當前從數字部分開始比較),則取出兩個字串的數字,按照數字大小進行對比。如果數字能夠比較出大小,則直接返回兩個字串的大小關係,不再對後面的字串進行對比。比如“0123aaa” 和“1bbbbbbbbb”,就直接返回“0123aaa”大於“1bbbbbbbbb”。當然,這裡取出的數字可能超出了uint64_t表示的最大值,但是這種概率很低,在我們的名稱排序中,很難遇到這麼長的數字進行比較的。明白這個規則後,大家對字串中出現的數字在進行排序時應該比較理解了。下面的名字排序是對著的。

img

綜述

本文主要講述由localizedStandardCompare:這個蘋果系統方法所引發的對排序規則的深入研究,簡單來說,設定中選擇區域為中國時,排序順序為 標點符號等特殊符號>數字>中文>英文等其他。中文字身是按照pinyin排序的,只是由於多音字的關係,不能夠做到100%按照中文習慣來排序,會有些無法正確排序的問題,但大體已經符合我們的習慣了。

參考

zh.wikipedia.org/wiki/Unicod…

developer.apple.com/library/arc…

www.objc.io/issues/9-st…

unicode.org/reports/tr1…

www.cnblogs.com/huahuahu/p/…

raw.githubusercontent.com/larvit/larv…

cldr.unicode.org/

相關閱讀 【每日課程推薦】機器學習實戰!快速入門線上廣告業務及CTR相應知識

此文已由作者授權騰訊雲+社群釋出,更多原文請點選

搜尋關注公眾號「雲加社群」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!

海量技術實踐經驗,盡在雲加社群

相關文章