
僅以此文,祭奠線上無限crash的61位用戶。
恩,先放重點(diǎn):
中文字符串比較,請(qǐng)使用-localizedCompare:方法。這一個(gè)系統(tǒng)方法足矣!
-localizedCompare:方法。這一個(gè)系統(tǒng)方法足矣!2017.05.24更新
-localizedCompare:這個(gè)方法能保證排序結(jié)果與系統(tǒng)通訊錄排序結(jié)果相同,基本符合拼音順序,但偶爾有偏差。
感謝 @半江瑟瑟 提供的測(cè)試數(shù)據(jù)立冬、李東、李Dong
想做到與系統(tǒng)排序方式保持一致請(qǐng)使用-localizedCompare:方法,想做到完美拼音排序請(qǐng)使用老司機(jī)文中提到的逐字比較方式。
恩,重點(diǎn)說完開始講故事,這篇文章主要用來總結(jié)幾種中文字符串比較的方法,以防以后我那次遇到什么特殊的需求。
這個(gè)故事中你將會(huì)看到:
- 字符串轉(zhuǎn)拼音
- -caseInsensitiveCompare:
- UILocalizedIndexedCollation
- 逐字比較
- GB_18030編碼
- -localizedCompare:
然而知識(shí)點(diǎn)只有:
- 字符串轉(zhuǎn)拼音
- -localizedCompare:
那個(gè)手機(jī)瀏覽的同志注意了,看到字符串轉(zhuǎn)拼音后就可以打住了,下面的內(nèi)容多圖殺貓費(fèi)流量=。=
事情是這樣的,需求要求自定義通訊錄選擇流程,故無法直接調(diào)用系統(tǒng)通訊錄。老司機(jī)自告奮勇的接下了活,畢竟腦袋一想還不難,可老司機(jī)低估了中文排序的坑=。=
1.最初的想法
最開始老司機(jī)想,首先所有聯(lián)系人都會(huì)按姓名首字母分組,似乎需要轉(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),請(qǐng)將NSDiacriticInsensitiveSearch替換為NSCaseInsensitiveSearch)。
轉(zhuǎn)完拼音后,就可以調(diào)用-caseInsensitiveCompare:進(jìn)行比較了,老司機(jī)當(dāng)時(shí)真是美滋滋。
與-caseInsensitiveCompare:效果相同的還有一個(gè)專門為了TableView而存在的排序的類,叫做UILocalizedIndexedCollation。他也可以用來排序,使用起來也挺簡單:
NSArray *arr = [self getName];///只是將幾個(gè)字符串分別包裝成對(duì)象
UILocalizedIndexedCollation *localized = [UILocalizedIndexedCollation currentCollation];
NSArray *temparr = [localized sortedArrayFromArray:arr collationStringSelector:@selector(fullName)];
不過他是基于對(duì)象的,你要把字符串當(dāng)做某個(gè)對(duì)象的屬性才能排序。并且它存在下面兩個(gè)問題中的第一個(gè)問題。
不過有兩個(gè)問題:
- 同音不同字
表現(xiàn)是什么呢?比如說三個(gè)人,請(qǐng)看圖示:

這個(gè)結(jié)果明顯是不我們可以接受的。
恩,上面轉(zhuǎn)拼音的方法會(huì)在兩個(gè)字之間自動(dòng)加上一個(gè)空格。所以老司機(jī)發(fā)現(xiàn)可以把拼音分開。所以老司機(jī)在這里的想法是逐字比較。

這樣的話,結(jié)果就是理想結(jié)果了。不過還有第二個(gè)問題。。
- 中英結(jié)合的字符串
中英結(jié)合的字符串轉(zhuǎn)換成拼音以后效果跟預(yù)想的有一定偏差。什么表現(xiàn)呢?

為什么這樣呢?我們看到轉(zhuǎn)拼音的時(shí)候中英結(jié)合的是沒有空格的。
老司機(jī)遇到錯(cuò)誤平錯(cuò)誤,想到因?yàn)橹杏⒔Y(jié)合有問題,我處理一下字符串把中英文分開不就好了么?

這樣的話張Wicky就變成張 Wicky轉(zhuǎn)成拼音就變成zhang wicky。排序完成。
然而我的61位用戶就是因?yàn)槲疫@一時(shí)大意而受到了無限crash的折磨。。。
矛盾點(diǎn)在這,比如用戶本來存的名字叫做張 啊。沒錯(cuò),就是名字里面本身就有一個(gè)空格(這61位用戶你們?yōu)槊婵崭癜 ?。。其他用戶怎么就不存呢。。一定是你不?huì)用),經(jīng)過上面的添加空格就會(huì)變成張 啊(名字中間變成了3個(gè)空格)。其實(shí)到這里還好,最可氣的是-componentsSeparatedByString:這個(gè)方法的行為跟老司機(jī)想的不一致啊。(敲黑板,重點(diǎn)了?。?/p>
同學(xué)們,張 啊這個(gè)字符串調(diào)用-componentsSeparatedByString:這個(gè)方法,傳參@" ",你們的理想結(jié)果是什么?

是的,比預(yù)想的多了兩個(gè)空字符串。。。問題很嚴(yán)重,原本張 啊字符串長度為3,拼音數(shù)組元素個(gè)數(shù)為4。然而后面有調(diào)用了-substringWithRange:方法。。。是的你沒猜錯(cuò),越界了。。。
到這想填坑其實(shí)還可以,只要在添加空格以后再檢驗(yàn)是否有連續(xù)空格,替換成一個(gè)空格就好了。。。不過這種打補(bǔ)丁,讓代碼越來越失去可維護(hù)性的做法老司機(jī)覺得是個(gè)隱患。。。所以老司機(jī)不得不想出第二個(gè)方法。
2.逐字比較時(shí)確保字與拼音一一對(duì)應(yīng)
最初的想法因?yàn)樵浇绯鰡栴},那么我是否讓字與拼音一一對(duì)應(yīng)上就好了呢?
那么首先要把字符串分成一個(gè)字一個(gè)字的,但是單詞還要保證是單詞而不是字母。

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

到了這里,因?yàn)橄炔鹱?,所以不需要手?dòng)添加空格,也避免了-substringWithRange:方法,所以根本就不存在越界了??雌饋硭坪醣茸畛醯南敕ㄊ×撕芏嗍?,老司機(jī)心里美滋滋。
多說一嘴,-enumerateSubstringsInRange:這個(gè)方法的行為很詭異,不知道是bug還是什么原理,表現(xiàn)如下:

當(dāng)?shù)谝粋€(gè)可見字符為漢字且緊跟著一個(gè)單詞的時(shí)候,這里面的子串都中文和英文是不會(huì)分開的,且后面的子串不熟影響。其他情況下都可以正常返回子串。
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) {///為防止第一個(gè)字與英文連在一起
[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ī)都快瘋了,媽媽,不要再讓我給字符串排序了。。。
又開始翻閱博客如何排序啊。。。
之前考慮過這個(gè)方法 但問題是不能對(duì)首字母之后的拼音排序 而且需要引用額外的文件 比較麻煩。
后來查到gb編碼本來就是用拼音排序的就hack了一下:在stringByAddingPercentEscapesUsingEncoding:后面用16位編碼 將中文轉(zhuǎn)為ascii來比較 更簡潔。
\#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í)棒!不過老司機(jī)真的有做測(cè)試的潛質(zhì),我也不知道為什么,我就隨便改了一下數(shù)據(jù),我都不知道怎么想的把往字改成了彺字結(jié)果就又錯(cuò)了。。。想想可能GB_18030這個(gè)標(biāo)準(zhǔn)也不都是按照拼音排的吧。。。
4.最后的,也是最簡單的,系統(tǒng)放在那我就一直沒用的。。。
最后的最后我又找到了這個(gè)方法,-localizedCompare:。真的是比什么都簡單,又比什么都對(duì)啊。這個(gè)方法沒什么bug也沒什么風(fēng)險(xiǎn)。。。簡單的不要不要的。。。
扣個(gè)題:
中文字符串比較,請(qǐng)使用-localizedCompare:方法。這一個(gè)系統(tǒng)方法足矣!
-localizedCompare:方法。這一個(gè)系統(tǒng)方法足矣!中文字符串比較,請(qǐng)使用-localizedCompare:方法。這一個(gè)系統(tǒng)方法足矣!
-localizedCompare:方法。這一個(gè)系統(tǒng)方法足矣!中文字符串比較,請(qǐng)使用-localizedCompare:方法。這一個(gè)系統(tǒng)方法足矣!
-localizedCompare:方法。這一個(gè)系統(tǒng)方法足矣!扣題改了,看下文章開頭的更新
想想自己因?yàn)橐雌匆舴纸M所以轉(zhuǎn)了拼音,之后就一直再以拼音排序,快要被自己蠢哭了。。。
