僅以此文,祭奠線上無限crash的61位用戶豺型。
恩,先放重點(diǎn):
中文字符串比較肴焊,請使用-localizedCompare:
方法娶眷。這一個系統(tǒng)方法足矣茂浮!
-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)是什么呢提针?比如說三個人嗜价,請看圖示:
這個結(jié)果明顯是不我們可以接受的届吁。
恩痴施,上面轉(zhuǎn)拼音的方法會在兩個字之間自動加上一個空格神得。所以老司機(jī)發(fā)現(xiàn)可以把拼音分開酝静。所以老司機(jī)在這里的想法是逐字比較让歼。
這樣的話倚评,結(jié)果就是理想結(jié)果了。不過還有第二個問題愕难。妖混。
- 中英結(jié)合的字符串
中英結(jié)合的字符串轉(zhuǎn)換成拼音以后效果跟預(yù)想的有一定偏差误褪。什么表現(xiàn)呢帜羊?
為什么這樣呢词裤?我們看到轉(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é)果是什么?
是的诊胞,比預(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來比較 更簡潔。
\#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)方法足矣讲竿!
-localizedCompare:
方法孽椰。這一個系統(tǒng)方法足矣讲竿!中文字符串比較,請使用-localizedCompare:
方法弄屡。這一個系統(tǒng)方法足矣题禀!
-localizedCompare:
方法弄屡。這一個系統(tǒng)方法足矣题禀!扣題改了,看下文章開頭的更新
想想自己因?yàn)橐雌匆舴纸M所以轉(zhuǎn)了拼音膀捷,之后就一直再以拼音排序迈嘹,快要被自己蠢哭了。。秀仲。