美文网首页其他一丢丢精华iOS学习笔记
老司机踩坑系列————中文排序

老司机踩坑系列————中文排序

作者: 老司机Wicky | 来源:发表于2017-05-21 19:15 被阅读793次
中文排序

仅以此文,祭奠线上无限crash的61位用户。

恩,先放重点:

中文字符串比较,请使用-localizedCompare:方法。这一个系统方法足矣!

2017.05.24更新
-localizedCompare:这个方法能保证排序结果与系统通讯录排序结果相同,基本符合拼音顺序,但偶尔有偏差。
感谢 @半江瑟瑟 提供的测试数据立冬、李东、李Dong
想做到与系统排序方式保持一致请使用-localizedCompare:方法,想做到完美拼音排序请使用老司机文中提到的逐字比较方式。

恩,重点说完开始讲故事,这篇文章主要用来总结几种中文字符串比较的方法,以防以后我那次遇到什么特殊的需求。

这个故事中你将会看到:

  • 字符串转拼音
  • -caseInsensitiveCompare:
  • UILocalizedIndexedCollation
  • 逐字比较
  • GB_18030编码
  • -localizedCompare:

然而知识点只有:

  • 字符串转拼音
  • -localizedCompare:

那个手机浏览的同志注意了,看到字符串转拼音后就可以打住了,下面的内容多图杀猫费流量=。=

事情是这样的,需求要求自定义通讯录选择流程,故无法直接调用系统通讯录。老司机自告奋勇的接下了活,毕竟脑袋一想还不难,可老司机低估了中文排序的坑=。=

1.最初的想法

最开始老司机想,首先所有联系人都会按姓名首字母分组,似乎需要转拼音。有了拼音就可以根据拼音排序,很顺畅的思路。Too young,Too naive。

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

转拼音老司机没有引用第三方库,用了三行代码就搞定了。(这样的方式转换出来的拼音是没有音调的,如果想要带着音调,请将NSDiacriticInsensitiveSearch替换为NSCaseInsensitiveSearch)。

转完拼音后,就可以调用-caseInsensitiveCompare:进行比较了,老司机当时真是美滋滋。

-caseInsensitiveCompare:效果相同的还有一个专门为了TableView而存在的排序的类,叫做UILocalizedIndexedCollation。他也可以用来排序,使用起来也挺简单:

NSArray *arr = [self getName];///只是将几个字符串分别包装成对象
UILocalizedIndexedCollation *localized =  [UILocalizedIndexedCollation currentCollation];
NSArray *temparr = [localized sortedArrayFromArray:arr collationStringSelector:@selector(fullName)];

不过他是基于对象的,你要把字符串当做某个对象的属性才能排序。并且它存在下面两个问题中的第一个问题。

不过有两个问题:

  • 同音不同字
    表现是什么呢?比如说三个人,请看图示:
转拼音后比较拼音

这个结果明显是不我们可以接受的。

恩,上面转拼音的方法会在两个字之间自动加上一个空格。所以老司机发现可以把拼音分开。所以老司机在这里的想法是逐字比较。

逐字比较

这样的话,结果就是理想结果了。不过还有第二个问题。。

  • 中英结合的字符串
    中英结合的字符串转换成拼音以后效果跟预想的有一定偏差。什么表现呢?
中英结合

为什么这样呢?我们看到转拼音的时候中英结合的是没有空格的。

老司机遇到错误平错误,想到因为中英结合有问题,我处理一下字符串把中英文分开不就好了么?

添加空格

这样的话张Wicky就变成张 Wicky转成拼音就变成zhang wicky。排序完成。

然而我的61位用户就是因为我这一时大意而受到了无限crash的折磨。。。

矛盾点在这,比如用户本来存的名字叫做张 啊。没错,就是名字里面本身就有一个空格(这61位用户你们为毛要存空格啊。。。其他用户怎么就不存呢。。一定是你不会用),经过上面的添加空格就会变成张 啊(名字中间变成了3个空格)。其实到这里还好,最可气的是-componentsSeparatedByString:这个方法的行为跟老司机想的不一致啊。(敲黑板,重点了啊)

同学们,张 啊这个字符串调用-componentsSeparatedByString:这个方法,传参@" ",你们的理想结果是什么?

实际结果

是的,比预想的多了两个空字符串。。。问题很严重,原本张 啊字符串长度为3,拼音数组元素个数为4。然而后面有调用了-substringWithRange:方法。。。是的你没猜错,越界了。。。

到这想填坑其实还可以,只要在添加空格以后再检验是否有连续空格,替换成一个空格就好了。。。不过这种打补丁,让代码越来越失去可维护性的做法老司机觉得是个隐患。。。所以老司机不得不想出第二个方法。

2.逐字比较时确保字与拼音一一对应

最初的想法因为越界出问题,那么我是否让字与拼音一一对应上就好了呢?
那么首先要把字符串分成一个字一个字的,但是单词还要保证是单词而不是字母。

分字

事实上老司机到这已经有了些许抗拒,为什么一个字符串排序就这么难。。。
到了这里思路大概就是这个样子的:

拆字

到了这里,因为先拆字,所以不需要手动添加空格,也避免了-substringWithRange:方法,所以根本就不存在越界了。看起来似乎比最初的想法省了很多事,老司机心里美滋滋。

多说一嘴,-enumerateSubstringsInRange:这个方法的行为很诡异,不知道是bug还是什么原理,表现如下:

奇怪的行为

当第一个可见字符为汉字且紧跟着一个单词的时候,这里面的子串都中文和英文是不会分开的,且后面的子串不熟影响。其他情况下都可以正常返回子串。

2017.05.25更新
有同学问具体是怎么实现的?老司机将中文拼音比较写在了字符串的扩展中。以下是.m中相关代码:

#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.带音调的拼音排序

上面的排序老司机都是在排没有音调的拼音。老司机在上面也有介绍过如果转换带音调的拼音方法,老司机又开始美滋滋的优化自己的代码了。想想不过是转拼音的时候转成带音调的然后源代码比较呗。结果。。。

什么鬼顺序

系统这是什么鬼顺序,开始怀疑小学老师教的āáǎà是假的了都。。老司机都快疯了,妈妈,不要再让我给字符串排序了。。。

又开始翻阅博客如何排序啊。。。

之前考虑过这个方法 但问题是不能对首字母之后的拼音排序 而且需要引用额外的文件 比较麻烦。

后来查到gb编码本来就是用拼音排序的就hack了一下:在stringByAddingPercentEscapesUsingEncoding:后面用16位编码 将中文转为ascii来比较 更简洁。

引自按照拼音对数组中的中文字符串排序的算法中Lunar川小槑的回复

\#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];
};

试了一下,诶,果然好使!顺序对的!也不用逐字比较了!一级棒!不过老司机真的有做测试的潜质,我也不知道为什么,我就随便改了一下数据,我都不知道怎么想的把往字改成了彺字结果就又错了。。。想想可能GB_18030这个标准也不都是按照拼音排的吧。。。

4.最后的,也是最简单的,系统放在那我就一直没用的。。。

最后的最后我又找到了这个方法,-localizedCompare:。真的是比什么都简单,又比什么都对啊。这个方法没什么bug也没什么风险。。。简单的不要不要的。。。

扣个题:

中文字符串比较,请使用-localizedCompare:方法。这一个系统方法足矣!

中文字符串比较,请使用-localizedCompare:方法。这一个系统方法足矣!

中文字符串比较,请使用-localizedCompare:方法。这一个系统方法足矣!

扣题改了,看下文章开头的更新

想想自己因为要按拼音分组所以转了拼音,之后就一直再以拼音排序,快要被自己蠢哭了。。。


蠢哭了蠢哭了

相关文章

  • 老司机踩坑系列————中文排序

    仅以此文,祭奠线上无限crash的61位用户。 恩,先放重点: 中文字符串比较,请使用-localizedComp...

  • 算法踩坑6-二叉搜索树排序

    背景 接上面五篇文章算法踩坑-快速排序 算法踩坑2-插入排序 算法踩坑3-堆排序 算法踩坑4-冒泡排序 ...

  • 算法踩坑5-归并排序

    背景 接上面四篇文章算法踩坑-快速排序 算法踩坑2-插入排序 算法踩坑3-堆排序 算法踩坑4-冒泡排序 来...

  • 算法踩坑4-冒泡排序

    背景 接上面三篇文章算法踩坑-快速排序 算法踩坑2-插入排序 算法踩坑3-堆排序 来继续聊聊最近我写的一些算...

  • 亚马逊收款账户修改怕审核?亚马逊卖家2017遇到的10大坑

    在亚马逊开店,不管小白还是老司机,踩“坑”早已习以为常,比如亚马逊改收款账户被审核。关于亚马逊收款,总是容易入坑。...

  • Retrofit Https踩坑记录

    Retrofit Https踩坑记录 前言 新司机上路,坑多,本文重点是踩坑,不详细讲retrofit用法,本文不...

  • 算法踩坑3-堆排序

    背景 接上面两篇文章算法踩坑-快速排序 算法踩坑2-插入排序 来继续聊聊最近我写的一些算法的小例程,这次要聊的...

  • 无标题文章

    1,配置maven setting.xml踩的坑,路径里面不要有中文

  • 踩坑系列

    1.jmeter线程组间数据传递:beanshell 的 __setProperty 2、mysql修改表结构关键...

  • java

    新手刚入坑,求老司机带

网友评论

  • 大刘:重庆和和大连哪个大?重是多音字,可以解释成“chong qing”或“zhong qing”.
    再比如长安,可解释成"change an"或"zhang an"
    对于多音字的处理。如果处置的呢。
    大刘:@老司机Wicky 我们目前也没有做到对多音字的处理,因为计算机是无法知道的,所以现在我们这边是通过平台返回拼音字段
    老司机Wicky:@愤怒的振振 因为当时只是做通讯录,所以后来只是针对姓氏做了一套多音字转换的方案
  • 阿群1986:Unicode和GB18030貌似都不完全按照拼音顺序排列
  • 春泥Fu::joy: :joy: :joy: 都是有故事的人才听懂心里的歌!
    老司机Wicky:@春泥Fu 灯光师已经准备好了,请说出你的故事:smirk:
  • 半江瑟瑟:最后如何实现微信或者原生 通讯录里面的排序 有中文 英文 特殊字符
  • 半江瑟瑟:想看demo
    半江瑟瑟:@老司机Wicky 我试试你说的最后一句话 😝
    半江瑟瑟:@老司机Wicky 我之前做过一个通讯录 但是只能做到按照拼音来排序 不能把一个姓氏的拍到一起,大神能帮忙写下demo吗
    老司机Wicky:@半江瑟瑟 这个博客没写demo:stuck_out_tongue_winking_eye:因为最后结论是调用一句话就可以啊
  • NSScorpio:我也想去 crash 一下,还有机会吗:joy:
    老司机Wicky:@NSScorpio 修复了哈哈
  • ifelseboyxx:哈哈哈
    老司机Wicky:@JohnsLee :sweat:你是在嘲笑那61个用户,嗯,一定是这样的
  • 微辣小龙虾:我不管。。。我看了第一行就来点赞。。笑尿我了:joy:
    老司机Wicky:@微辣小龙虾 :relieved:这是一个忧桑的故事
  • __夏至未至:系统的方法对于沈有点不是很友善啊,他转化为chen。。。
    老司机Wicky:@__夏至未至 是的,所以可以做一层转换后排序。在我的仓库中我写的通讯录工具类有对多音字的处理,感兴趣的话可以去瞧瞧:yum:
  • dedenc:学到了 很感谢
    老司机Wicky:@dedenc 共同学习:blush:
  • 春暖花已开:写得不错,谢谢楼主分享!
    老司机Wicky:@人民重重 感谢鼓励(◍•ᴗ•◍):heart:

本文标题:老司机踩坑系列————中文排序

本文链接:https://www.haomeiwen.com/subject/qetvxxtx.html