老司機(jī)踩坑系列————中文排序

中文排序

僅以此文,祭奠線上無限crash的61位用戶豺型。

恩,先放重點(diǎn):

中文字符串比較肴焊,請使用-localizedCompare:方法娶眷。這一個系統(tǒng)方法足矣茂浮!

2017.05.24更新
-localizedCompare:這個方法能保證排序結(jié)果與系統(tǒng)通訊錄排序結(jié)果相同席揽,基本符合拼音順序,但偶爾有偏差竟稳。
感謝 @半江瑟瑟 提供的測試數(shù)據(jù)立冬、李東果善、李Dong
想做到與系統(tǒng)排序方式保持一致請使用-localizedCompare:方法巾陕,想做到完美拼音排序請使用老司機(jī)文中提到的逐字比較方式鄙煤。

恩梯刚,重點(diǎn)說完開始講故事亡资,這篇文章主要用來總結(jié)幾種中文字符串比較的方法沟于,以防以后我那次遇到什么特殊的需求展懈。

這個故事中你將會看到:

  • 字符串轉(zhuǎn)拼音
  • -caseInsensitiveCompare:
  • UILocalizedIndexedCollation
  • 逐字比較
  • GB_18030編碼
  • -localizedCompare:

然而知識點(diǎn)只有:

  • 字符串轉(zhuǎn)拼音
  • -localizedCompare:

那個手機(jī)瀏覽的同志注意了冻记,看到字符串轉(zhuǎn)拼音后就可以打住了冗栗,下面的內(nèi)容多圖殺貓費(fèi)流量=。=

事情是這樣的供搀,需求要求自定義通訊錄選擇流程隅居,故無法直接調(diào)用系統(tǒng)通訊錄。老司機(jī)自告奮勇的接下了活葛虐,畢竟腦袋一想還不難胎源,可老司機(jī)低估了中文排序的坑=。=

1.最初的想法

最開始老司機(jī)想屿脐,首先所有聯(lián)系人都會按姓名首字母分組涕蚤,似乎需要轉(zhuǎn)拼音宪卿。有了拼音就可以根據(jù)拼音排序休溶,很順暢的思路义黎。Too young,Too naive。

///漢字轉(zhuǎn)拼音
-(NSString *)transferChineseToPinYin:(NSString *)string {
    NSMutableString *mutableString = [NSMutableString stringWithString:string];
    CFStringTransform((CFMutableStringRef)mutableString, NULL, kCFStringTransformToLatin, false);
    return [mutableString stringByFoldingWithOptions:NSDiacriticInsensitiveSearch locale:[NSLocale currentLocale]];
}

轉(zhuǎn)拼音老司機(jī)沒有引用第三方庫贡羔,用了三行代碼就搞定了。(這樣的方式轉(zhuǎn)換出來的拼音是沒有音調(diào)的逐虚,如果想要帶著音調(diào)涤伐,請將NSDiacriticInsensitiveSearch替換為NSCaseInsensitiveSearch)。

轉(zhuǎn)完拼音后,就可以調(diào)用-caseInsensitiveCompare:進(jìn)行比較了,老司機(jī)當(dāng)時(shí)真是美滋滋。

-caseInsensitiveCompare:效果相同的還有一個專門為了TableView而存在的排序的類,叫做UILocalizedIndexedCollation。他也可以用來排序,使用起來也挺簡單:

NSArray *arr = [self getName];///只是將幾個字符串分別包裝成對象
UILocalizedIndexedCollation *localized =  [UILocalizedIndexedCollation currentCollation];
NSArray *temparr = [localized sortedArrayFromArray:arr collationStringSelector:@selector(fullName)];

不過他是基于對象的,你要把字符串當(dāng)做某個對象的屬性才能排序屎篱。并且它存在下面兩個問題中的第一個問題。

不過有兩個問題:

  • 同音不同字
    表現(xiàn)是什么呢提针?比如說三個人嗜价,請看圖示:
轉(zhuǎn)拼音后比較拼音

這個結(jié)果明顯是不我們可以接受的届吁。

恩痴施,上面轉(zhuǎn)拼音的方法會在兩個字之間自動加上一個空格神得。所以老司機(jī)發(fā)現(xiàn)可以把拼音分開酝静。所以老司機(jī)在這里的想法是逐字比較让歼。

逐字比較

這樣的話倚评,結(jié)果就是理想結(jié)果了。不過還有第二個問題愕难。妖混。

  • 中英結(jié)合的字符串
    中英結(jié)合的字符串轉(zhuǎn)換成拼音以后效果跟預(yù)想的有一定偏差误褪。什么表現(xiàn)呢帜羊?
中英結(jié)合

為什么這樣呢词裤?我們看到轉(zhuǎn)拼音的時(shí)候中英結(jié)合的是沒有空格的周偎。

老司機(jī)遇到錯誤平錯誤拓瞪,想到因?yàn)橹杏⒔Y(jié)合有問題,我處理一下字符串把中英文分開不就好了么复亏?

添加空格

這樣的話張Wicky就變成張 Wicky轉(zhuǎn)成拼音就變成zhang wicky上祈。排序完成廊宪。

然而我的61位用戶就是因?yàn)槲疫@一時(shí)大意而受到了無限crash的折磨珍策。。料仗。

矛盾點(diǎn)在這,比如用戶本來存的名字叫做張 啊紫新。沒錯,就是名字里面本身就有一個空格(這61位用戶你們?yōu)槊婵崭癜 O志鳌3咂濉F渌脩粼趺淳筒淮婺亍:龆场R欢ㄊ悄悴粫茫┬懔猓?jīng)過上面的添加空格就會變成張 啊(名字中間變成了3個空格)琼锋。其實(shí)到這里還好荷腊,最可氣的是-componentsSeparatedByString:這個方法的行為跟老司機(jī)想的不一致啊勿璃。(敲黑板擒抛,重點(diǎn)了巴破)

同學(xué)們,張 啊這個字符串調(diào)用-componentsSeparatedByString:這個方法歧沪,傳參@" "歹撒,你們的理想結(jié)果是什么?

實(shí)際結(jié)果

是的诊胞,比預(yù)想的多了兩個空字符串暖夭。。撵孤。問題很嚴(yán)重迈着,原本張 啊字符串長度為3,拼音數(shù)組元素個數(shù)為4邪码。然而后面有調(diào)用了-substringWithRange:方法裕菠。。闭专。是的你沒猜錯奴潘,越界了。影钉。画髓。

到這想填坑其實(shí)還可以,只要在添加空格以后再檢驗(yàn)是否有連續(xù)空格平委,替換成一個空格就好了奈虾。。肆汹。不過這種打補(bǔ)丁愚墓,讓代碼越來越失去可維護(hù)性的做法老司機(jī)覺得是個隱患。昂勉。浪册。所以老司機(jī)不得不想出第二個方法。

2.逐字比較時(shí)確保字與拼音一一對應(yīng)

最初的想法因?yàn)樵浇绯鰡栴}岗照,那么我是否讓字與拼音一一對應(yīng)上就好了呢村象?
那么首先要把字符串分成一個字一個字的,但是單詞還要保證是單詞而不是字母攒至。

分字

事實(shí)上老司機(jī)到這已經(jīng)有了些許抗拒厚者,為什么一個字符串排序就這么難。迫吐。库菲。
到了這里思路大概就是這個樣子的:

拆字

到了這里,因?yàn)橄炔鹱种景颍圆恍枰謩犹砑涌崭裎跤睿脖苊饬?code>-substringWithRange:方法鳖擒,所以根本就不存在越界了√讨梗看起來似乎比最初的想法省了很多事蒋荚,老司機(jī)心里美滋滋。

多說一嘴馆蠕,-enumerateSubstringsInRange:這個方法的行為很詭異期升,不知道是bug還是什么原理,表現(xiàn)如下:

奇怪的行為

當(dāng)?shù)谝粋€可見字符為漢字且緊跟著一個單詞的時(shí)候互躬,這里面的子串都中文和英文是不會分開的播赁,且后面的子串不熟影響。其他情況下都可以正常返回子串吼渡。

2017.05.25更新
有同學(xué)問具體是怎么實(shí)現(xiàn)的行拢?老司機(jī)將中文拼音比較寫在了字符串的擴(kuò)展中。以下是.m中相關(guān)代碼:

#define replaceIfContain(string,target,replacement,tone) \
do {\
if ([string containsString:target]) {\
string = [string stringByReplacingOccurrencesOfString:target withString:replacement];\
string = [NSString stringWithFormat:@"%@%d",string,tone];\
}\
} while(0)

@interface NSString ()
@property (nonatomic ,strong) NSArray * wordArray;
@property (nonatomic ,copy) NSString * wordPinyinWithTone;
@property (nonatomic ,copy) NSString * wordPinyinWithoutTone;
@end

@implementation NSString (DWStringSortUtils)
-(NSComparisonResult)dw_ComparedInPinyinWithString:(NSString *)string considerTone:(BOOL)tone {
    if ([self isEqualToString:string]) {
        return NSOrderedSame;
    }
    NSArray <NSString *>* arr1 = self.wordArray;
    NSArray <NSString *>* arr2 = string.wordArray;
    NSUInteger minL = MIN(arr1.count, arr2.count);
    for (int i = 0; i < minL; i ++) {
        if ([arr1[i] isEqualToString:arr2[i]]) {
            continue;
        }
        NSString * pinyin1 = [arr1[i] transferWordToPinYinWithTone:tone];
        NSString * pinyin2 = [arr2[i] transferWordToPinYinWithTone:tone];
        if (tone) {
            pinyin1 = transformPinyinTone(pinyin1);
            pinyin2 = transformPinyinTone(pinyin2);
        }
        NSComparisonResult result = [pinyin1 caseInsensitiveCompare:pinyin2];
        if (result != NSOrderedSame) {
            return result;
        } else {
            result = [arr1[i] localizedCompare:arr2[i]];
            if (result != NSOrderedSame) {
                return result;
            }
        }
    }
    if (arr1.count < arr2.count) {
        return NSOrderedAscending;
    } else if (arr1.count > arr2.count) {
        return NSOrderedDescending;
    } else {
        return NSOrderedSame;
    }
}
#pragma mark --- tool method ---
-(NSString *)transferWordToPinYinWithTone:(BOOL)tone {
    if (tone && self.wordPinyinWithTone) {
        return self.wordPinyinWithTone;
    } else if (!tone && self.wordPinyinWithoutTone) {
        return self.wordPinyinWithoutTone;
    }
    NSMutableString * mutableString = [[NSMutableString alloc] initWithString:self];
    CFStringTransform((CFMutableStringRef)mutableString, NULL, kCFStringTransformToLatin, false);
    NSStringCompareOptions toneOption = tone ?NSCaseInsensitiveSearch:NSDiacriticInsensitiveSearch;
    NSString * pinyin = [mutableString stringByFoldingWithOptions:toneOption locale:[NSLocale currentLocale]];
    if (tone) {
        self.wordPinyinWithTone = pinyin;
    } else {
        self.wordPinyinWithoutTone = pinyin;
    }
    return pinyin;
}
-(BOOL)dw_StringIsChinese {
    if (self.length == 0) {
        return NO;
    }
    NSPredicate * predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",@"[\\u4E00-\\u9FA5]+"];
    return [predicate evaluateWithObject:self];
}
-(NSArray *)dw_TrimStringToWord {
    if (self.length) {
        NSMutableArray * temp = [NSMutableArray array];
        [self enumerateSubstringsInRange:NSMakeRange(0, self.length) options:NSStringEnumerationByWords usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
            if (substring.length > 1 && temp.count == 0 && ![substring dw_StringIsChinese] && [substring dw_SubStringConfirmToPattern:@"[\\u4E00-\\u9FA5]+"].count > 0) {///為防止第一個字與英文連在一起
                [temp addObject:[substring substringToIndex:1]];
                [temp addObject:[substring substringFromIndex:1]];
            } else {
                if (substring.length > 1 && [substring dw_StringIsChinese]) {
                    [substring enumerateSubstringsInRange:NSMakeRange(0, substring.length) options:(NSStringEnumerationByComposedCharacterSequences) usingBlock:^(NSString * _Nullable substring2, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
                        [temp addObject:substring2];
                    }];
                } else {
                    if (substring.length) {
                        [temp addObject:substring];
                    }
                }
            }
        }];
        return [temp copy];
    }
    return nil;
}
#pragma mark --- inline method ---
static inline NSString * transformPinyinTone(NSString * pinyin) {
    replaceIfContain(pinyin, @"ā", @"a",1);
    replaceIfContain(pinyin, @"á", @"a",2);
    replaceIfContain(pinyin, @"ǎ", @"a",3);
    replaceIfContain(pinyin, @"à", @"a",4);
    replaceIfContain(pinyin, @"ō", @"o",1);
    replaceIfContain(pinyin, @"ó", @"o",2);
    replaceIfContain(pinyin, @"ǒ", @"o",3);
    replaceIfContain(pinyin, @"ò", @"o",4);
    replaceIfContain(pinyin, @"ē", @"e",1);
    replaceIfContain(pinyin, @"é", @"e",2);
    replaceIfContain(pinyin, @"ě", @"e",3);
    replaceIfContain(pinyin, @"è", @"e",4);
    replaceIfContain(pinyin, @"ī", @"i",1);
    replaceIfContain(pinyin, @"í", @"i",2);
    replaceIfContain(pinyin, @"ǐ", @"i",3);
    replaceIfContain(pinyin, @"ì", @"i",4);
    replaceIfContain(pinyin, @"ū", @"u",1);
    replaceIfContain(pinyin, @"ú", @"u",2);
    replaceIfContain(pinyin, @"ǔ", @"u",3);
    replaceIfContain(pinyin, @"ù", @"u",4);
    return pinyin;
}
#pragma mark ---setter/getter ---
-(void)setWordPinyinWithTone:(NSString *)wordPinyinWithTone {
    objc_setAssociatedObject(self, @selector(wordPinyinWithTone), wordPinyinWithTone, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

-(NSString *)wordPinyinWithTone {
    return objc_getAssociatedObject(self, _cmd);
}

-(void)setWordPinyinWithoutTone:(NSString *)wordPinyinWithoutTone {
    objc_setAssociatedObject(self, @selector(wordPinyinWithoutTone), wordPinyinWithoutTone, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

-(NSString *)wordPinyinWithoutTone {
    return objc_getAssociatedObject(self, _cmd);
}

-(void)setWordArray:(NSArray *)wordArray {
    objc_setAssociatedObject(self, @selector(wordArray), wordArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(NSArray *)wordArray {
    NSArray * array = objc_getAssociatedObject(self, _cmd);
    if (!array) {
        array = [self dw_TrimStringToWord];
        objc_setAssociatedObject(self, @selector(wordArray), array, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return array;
}
@end

3.帶音調(diào)的拼音排序

上面的排序老司機(jī)都是在排沒有音調(diào)的拼音诞吱。老司機(jī)在上面也有介紹過如果轉(zhuǎn)換帶音調(diào)的拼音方法,老司機(jī)又開始美滋滋的優(yōu)化自己的代碼了竭缝。想想不過是轉(zhuǎn)拼音的時(shí)候轉(zhuǎn)成帶音調(diào)的然后源代碼比較唄房维。結(jié)果。抬纸。咙俩。

什么鬼順序

系統(tǒng)這是什么鬼順序,開始懷疑小學(xué)老師教的āáǎà是假的了都湿故。阿趁。老司機(jī)都快瘋了,媽媽坛猪,不要再讓我給字符串排序了脖阵。。墅茉。

又開始翻閱博客如何排序啊命黔。。就斤。

之前考慮過這個方法 但問題是不能對首字母之后的拼音排序 而且需要引用額外的文件 比較麻煩悍募。

后來查到gb編碼本來就是用拼音排序的就hack了一下:在stringByAddingPercentEscapesUsingEncoding:后面用16位編碼 將中文轉(zhuǎn)為ascii來比較 更簡潔。

引自按照拼音對數(shù)組中的中文字符串排序的算法中Lunar川小槑的回復(fù)

\#define GB18030_ENCODING CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000)
 
// 其他代碼...
 
NSComparator comparator = ^(NSString *obj1, NSString *obj2){
 
        NSString *str1 = [obj1 stringByAddingPercentEscapesUsingEncoding:GB18030_ENCODING];
        NSString *str2 = [obj2 stringByAddingPercentEscapesUsingEncoding:GB18030_ENCODING];
 
        return [str1 compare:str2];
};

試了一下洋机,誒坠宴,果然好使!順序?qū)Φ谋疗欤∫膊挥弥鹱直容^了喜鼓!一級棒副砍!不過老司機(jī)真的有做測試的潛質(zhì),我也不知道為什么颠通,我就隨便改了一下數(shù)據(jù)址晕,我都不知道怎么想的把往字改成了彺字結(jié)果就又錯了。顿锰。谨垃。想想可能GB_18030這個標(biāo)準(zhǔn)也不都是按照拼音排的吧。硼控。刘陶。

4.最后的,也是最簡單的牢撼,系統(tǒng)放在那我就一直沒用的匙隔。。熏版。

最后的最后我又找到了這個方法纷责,-localizedCompare:。真的是比什么都簡單撼短,又比什么都對啊再膳。這個方法沒什么bug也沒什么風(fēng)險(xiǎn)。曲横。喂柒。簡單的不要不要的。禾嫉。灾杰。

扣個題:

中文字符串比較,請使用-localizedCompare:方法熙参。這一個系統(tǒng)方法足矣艳吠!

中文字符串比較,請使用-localizedCompare:方法孽椰。這一個系統(tǒng)方法足矣讲竿!

中文字符串比較,請使用-localizedCompare:方法弄屡。這一個系統(tǒng)方法足矣题禀!

扣題改了,看下文章開頭的更新

想想自己因?yàn)橐雌匆舴纸M所以轉(zhuǎn)了拼音膀捷,之后就一直再以拼音排序迈嘹,快要被自己蠢哭了。。秀仲。


蠢哭了
蠢哭了
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末融痛,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子神僵,更是在濱河造成了極大的恐慌雁刷,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件保礼,死亡現(xiàn)場離奇詭異沛励,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)炮障,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門目派,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人胁赢,你說我怎么就攤上這事企蹭。” “怎么了智末?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵谅摄,是天一觀的道長。 經(jīng)常有香客問我系馆,道長螟凭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任它呀,我火速辦了婚禮,結(jié)果婚禮上棒厘,老公的妹妹穿的比我還像新娘纵穿。我一直安慰自己,他們只是感情好奢人,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布谓媒。 她就那樣靜靜地躺著,像睡著了一般何乎。 火紅的嫁衣襯著肌膚如雪句惯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天支救,我揣著相機(jī)與錄音抢野,去河邊找鬼。 笑死各墨,一個胖子當(dāng)著我的面吹牛指孤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼恃轩,長吁一口氣:“原來是場噩夢啊……” “哼结洼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起叉跛,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤松忍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后筷厘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸣峭,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年敞掘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了叽掘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡玖雁,死狀恐怖更扁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赫冬,我是刑警寧澤浓镜,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站劲厌,受9級特大地震影響膛薛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜补鼻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一哄啄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧风范,春花似錦咨跌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至寇漫,卻和暖如春刊殉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背州胳。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工记焊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人栓撞。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓亚亲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子捌归,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,510評論 25 707
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法肛响,類相關(guān)的語法,內(nèi)部類的語法惜索,繼承相關(guān)的語法特笋,異常的語法,線程的語...
    子非魚_t_閱讀 31,581評論 18 399
  • 第5章 引用類型(返回首頁) 本章內(nèi)容 使用對象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學(xué)一百閱讀 3,212評論 0 4
  • 每個圈子里一定都有這樣一個人: 他活潑熱情,能輕易和每個人都熟絡(luò)角塑;他深明大義蔫磨,在各種矛盾里總能把一碗水端平;他替人...
    吳困困閱讀 15,437評論 207 263
  • 還有不到兩個月就畢業(yè)一年了圃伶〉倘纾回想起來,大四還是成長不少的窒朋,很多奇奇怪怪的找到答案或沒找到答案的問題都是那時(shí)想過的搀罢。...
    大蝦和小俠閱讀 207評論 0 0