最近项目做语音识别下单的功能,但是数字识别出来是以中文汉字的形式展示的,但是输入框是要展示阿拉伯数字的样式传给服务器。
怎么实现中文数字转阿拉伯数字呢?我们来捋一捋,思路是这样的无非两个大的步骤:
1.从一长串字符串中提取中文数字:零一二三四五六七八九十百千万亿;
2.将提取的中文数字字符转换成阿拉伯数字,然后组合起来,且要等价于之前的中文数字大小。
首先我们定义两个字典,以键值对的形式存起来,key为中文,value为数字。第一个字典是零到九的十进制中-阿数字。第二字典的每个元素可以看做是第一个字典元素的单位,其对应的值也是个字典,value是单位对应的大小权数,secUnit是个bool值其含义是是否为万或者亿。至于为什么要区分万、亿?是因为通常数字我们都是4位一分割,万、亿是个特殊节点,看后面的算法大家可能就会明白。
NSDictionary *chnNumChar = @{@"零":@0,@"一":@1,@"二":@2,@"两":@2,@"三":@3,@"四":@4,@"五":@5,@"六":@6,@"七":@7,@"八":@8,@"九":@9};
NSDictionary *chnNameValue = @{
@"十":@{@"value":@10, @"secUnit":@(false)},
@"百":@{@"value":@100, @"secUnit":@(false)},
@"千":@{@"value":@1000, @"secUnit":@(false)},
@"万":@{@"value":@10000, @"secUnit":@(true)},
@"亿":@{@"value":@100000000, @"secUnit":@(true)},
};
我们先分析上面的第2点如何传入中文提取到的中文中文字符串转数字,至于如何提取到中文数字我们后面讲,这个比较简单。既然我们上面定义了字典,很显然我们要拿到单个字符,肯定是要遍历字符串的。
//遍历字符串,单个字符存入数组
NSMutableArray *strArrM = [NSMutableArray array];
for (NSInteger i = 0;i < chnStr.length;i ++) {
NSString *charStr = [chnStr substringWithRange:NSMakeRange(i, 1)];
//此处字符筛选下比较好,因为某些原因可能存在非数字字符
if (chnNumChar[charStr] != nil || chnNameValue[charStr] != nil) {
[strArrM addObject:charStr];
}
}
拿到单个字符后,我们要做的就是将数组遍历,然后计算字符转换后计算的总值。大概思路是如果是取到key对应的值,如果是十进制数字就用变量number存起来,后面肯定会接一个单位数值,如果是十、百、千就拿前面的number和其对应的权数相乘用section接收,在没遇到万、亿之前累加section += (number * unit);当字符为万、亿就拿之前累加的和乘以权数,并存储在变量rtn += section;此时section置零开启下次循环。每次循环number应置零。若万、亿后再无如何字符,rtn便是最终结果,若有应加上section的计算值。笔者中文表述水平有限,直接上代码,大家可能看的比较清楚,做了必要注释:
NSInteger number = 0;//用以接收下面循环遍历的数字字符的数值
NSInteger section = 0;//用以万、亿字符节点前求和
NSInteger rtn = 0;//若万、亿后再无如何字符,rtn便是最终结果,若有应加上section的计算值
BOOL secUnit = false;//字符是否为万、亿
//遍历字符数组
for(NSInteger i = 0; i < strArrM.count; i++){
//取出中文数字字符
NSNumber *temNum = chnNumChar[strArrM[i]];
NSInteger num = [temNum integerValue];
if(temNum != nil){ //中文数字字符
number = num;
//若为最后一个字符直接相加
if(i == strArrM.count - 1){
section += number;
}
}else{ //中文单位字符
//取出单位字符对应的数值
NSNumber *temUnit = chnNameValue[strArrM[i]][@"value"];
NSInteger unit = [temUnit integerValue];
//取出单位字符对应的类型
NSNumber *temSecUnit = chnNameValue[strArrM[i]][@"secUnit"];
secUnit = [temSecUnit boolValue];
if(!secUnit){
//单位为十、百、千,拿数字值乘以单位数值,并累加
section += (number * unit);
}else{
//单位为万、亿,拿前面的数值累加的结果乘以对应的单位数值
section = (section + number) * unit;
//累加
rtn += section;
//用后置零
section = 0;
}
//用后置零
number = 0;
}
}
NSLog(@"中文数字:%@,阿拉伯数字:%ld",chnStr,rtn + section);
如此我们就完成了汉字转阿拉伯数字。
在后面的实测中发现一个问题,如果我们说十,十万,十亿等“十”前面没有十进制字符或者单纯“十”前面只有一个“零”的时候我们转换的结果是当前节点的值为0。通过观察前面的算法不难发现十作为单位字符前面肯定要跟数字字符系数的不然系数就相当于0,零十相当于0 * 10。所以我们应该判断十所在位置的前一个字符的值,前面没有“一”到“九”或者前面是个零的应该在“十”前面追加个“一”存入数组:
//遍历字符串用数组接收单个字符
NSMutableArray *strArrM = [NSMutableArray array];
for (NSInteger i = 0;i < chnStr.length;i ++) {
NSString *charStr = [chnStr substringWithRange:NSMakeRange(i, 1)];
if (chnNumChar[charStr] != nil || chnNameValue[charStr] != nil) {
NSString *tempStr = [chnStr substringWithRange:NSMakeRange(i - 1, 1)];
//如果第一个字符为十则在其前面添‘一’;如果字符“十”的前一个字符为“零”或者前面没有数字字符则在其前面添“一”
if (i == 0 && [charStr isEqual:@"十"]) {
[strArrM addObject:@"一"];
}else if( i > 0 && (chnNumChar[tempStr] == nil || [tempStr isEqual:@"零"]) && [charStr isEqual:@"十"]){
[strArrM addObject:@"一"];
}
[strArrM addObject:charStr];
}
}
中文数字转阿拉伯数字相信大家应该都明白了,回到前面我们如何把普通汉字和数字字符分开呢?我们分析下,一句话中可能存在中文英文数字和标点,为了便于转换我们先把标点去除,得到无标点的字符后再遍历,不是数字字符的就用数组cnArr存起来,遇到是数字字符的时候我们也把它用数组digitalArr存起来,如果这个非数字字符前一个字符是数字字符,我们就把之前的digitalArr,cnArr用componentsJoinedByString方法分别合并成字符串,存储在数组中。
//正则匹配标点
NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:@"@//:;()¥「」"、【】;“[]{}#%-*+=_\\|~<>$€^•'@#$%^&*()_+'\",,.。??"];
//去除标点合并
NSString *trimmedString = [[str componentsSeparatedByCharactersInSet:set] componentsJoinedByString: @""];
NSLog(@"%@",trimmedString);
//此字典增加了阿拉伯数字,因为一句话中可能也存在不是中文数字的我们只需要挑出来,并不需要再去转阿拉伯数字了
NSDictionary *chnNumChar = @{@"零":@0,@"一":@1,@"二":@2,@"两":@2,@"三":@3,@"四":@4,@"五":@5,@"六":@6,@"七":@7,@"八":@8,@"九":@9,@"0":@0,@"1":@1,@"2":@2,@"3":@3,@"4":@4,@"5":@5,@"6":@6,@"7":@7,@"8":@8,@"9":@9};
NSDictionary *chnNameValue = @{
@"十":@{@"value":@10, @"secUnit":@(false)},
@"百":@{@"value":@100, @"secUnit":@(false)},
@"千":@{@"value":@1000, @"secUnit":@(false)},
@"万":@{@"value":@10000, @"secUnit":@(true)},
@"亿":@{@"value":@100000000, @"secUnit":@(true)},
};
NSMutableArray *allDic = [NSMutableArray array];//汉字对应的数字数组
NSMutableArray *cnArr = [NSMutableArray array];//汉字数组
NSMutableArray *digitalArr = [NSMutableArray array];//�数字数组
BOOL isNum = NO;//是否是数字
for (NSInteger i = 0; i < trimmedString.length; i ++) {
NSString *charStr = [trimmedString substringWithRange:NSMakeRange(i, 1)];
if (chnNumChar[charStr] != nil || chnNameValue[charStr] != nil) {
isNum = YES;
[digitalArr addObject:charStr];
if (i == trimmedString.length - 1) {
[allDic addObject:@[[cnArr componentsJoinedByString:@""],[digitalArr componentsJoinedByString:@""]]];
[cnArr removeAllObjects];
[digitalArr removeAllObjects];
}
}else{
if (i > 0 && isNum == YES) {
[allDic addObject:@[[cnArr componentsJoinedByString:@""],[digitalArr componentsJoinedByString:@""]]];
[cnArr removeAllObjects];
[digitalArr removeAllObjects];
isNum = NO;
}
[cnArr addObject:charStr];
}
}
NSString *endStr = @"\n";
for (NSArray *valu in allDic) {
NSInteger number = 0;
if ([valu[1] integerValue] > 0) {
number = [valu[1] integerValue];
//科大讯飞,识别万的问题
if([valu[1] hasSuffix:@"万"]){
number = [valu[1] integerValue] * 10000;
}
}else{
//chineseNumbersReturnArabicNumerals方法为最上面提到自定义的中文数字转阿拉伯数字
number = [self chineseNumbersReturnArabicNumerals:valu[1]];
}
endStr = [endStr stringByAppendingString:[NSString stringWithFormat:@"%@ = %ld;\n",valu[0],number]];
}
NSLog(@"%@",endStr);
/*以@"件数十,毛重一千..。计费重量一千."为例遍历打印的结果就是:
件数 = 10;
毛重 = 1000;
计费重量 =1000;
*/
========= 分割线 (2018.4.24更新) =========
上面那种算法是从高位取到低位,下面提供一种从低位到高位取值转换的方法,感兴趣的同学可以看下,已做详细注释。算法我只测试了一些比较有代表性的数据,可能存在不严谨局限的地方,如要接入项目请斟酌:
//测试数据
chnStr = @"十一亿一千一百一十一万一千一百一十一";
NSDictionary *chnNumChar = @{@"零":@0,@"一":@1,@"二":@2,@"两":@2,@"三":@3,@"四":@4,@"五":@5,@"六":@6,@"七":@7,@"八":@8,@"九":@9,@"十":@10,@"百":@100,@"千":@1000,@"万":@10000,@"亿":@100000000};
//遍历字符串用数组接收单个字符
NSMutableArray *strArrM = [NSMutableArray array];
for (NSInteger i = 0;i < chnStr.length;i ++) {
NSString *charStr = [chnStr substringWithRange:NSMakeRange(i, 1)];
if (chnNumChar[charStr] != nil) {
NSString *tempStr = [chnStr substringWithRange:NSMakeRange(i - 1, 1)];
//如果第一个字符为十则在其前面添‘一’;如果字符“十”的前一个字符为“零”或者前面没有数字字符则在其前面添“一”
if (i == 0 && [charStr isEqual:@"十"]) {
[strArrM addObject:@"一"];
}else if( i > 0 && (chnNumChar[tempStr] == nil || [tempStr isEqual:@"零"]) && [charStr isEqual:@"十"]){
[strArrM addObject:@"一"];
}
[strArrM addObject:charStr];
}
}
NSArray *arr = [[strArrM reverseObjectEnumerator] allObjects];//数组倒序
NSInteger total = 0;//总值
NSInteger r = 1;//位权
NSInteger u = 1;//记录单位节点
for (NSInteger i = 0; i < arr.count; i ++) {
NSInteger val = [chnNumChar[arr[i]] integerValue];//从右至左(从低位到高位)逐位取值 ←----
if (val >= 10){ //单位字符
if (val > r) { //如果此时的字符单位值大于之前的位权
//把单位值赋值给位权r,并记录此时的最大单位u
r = val;
u = val;
}else{ //如果此时的字符单位值不大于之前的位权
//此前的最大单位u与此时的字符单位的乘积即为此时的位权
r = u * val;
}
}else{ //数字字符
//累加计算当前的总值
total += r * val;
//NSLog(@"%ld",total);
}
}
NSLog(@"\n====\n 中文数字: %@ ;\n 阿拉伯数字:%ld 。\n====",chnStr,total);
本文是根据科大讯飞语音识别的特点对应写的程序,苹果自带的语音识别或者别家公司的语音识别SDK可能会有问题,要使用的同学可针对性进行代码的修改。
代码:
码云: https://gitee.com/Leesonpeng/cnReturnNum
github: https://github.com/leesonp/CNReturnNumber
有什么不足的地方欢迎探讨指正,谢谢。