你写的字符(串)忽略大小写比较函数真的严谨吗?

提示

阅读本文需要同时对c++和java有一定了解。

背景

有时我们比较两个字符串时不考虑它们是大写还是小写;举个例子,在这种情况下我们认为“BanAna”和“baNaNA”是等价的。

其中一种思路是:

1. 将两个字符串都转换为小写(或者都转换为大写);

2.比较转换后的两个字符串是否相同。

这里给出一段C++示例代码:

//C++ example that we offen use

bool testIgnoreCase(string str1, string str2){
    transform(str1.begin(),str1.end(),str1.begin(),::tolower);
    transform(str2.begin(),str2.end(),str2.begin(),::tolower);

    //Or
    //transform(str1.begin(),str1.end(),str1.begin(),::toupper);
    //transform(str2.begin(),str2.end(),str2.begin(),::toupper);

    cout<<str1<<" "<<str2<<endl;//apple apple
    return str1 == str2;
}

int main()
{
    string str1 = "ApplE";
    string str2 = "apPle";
    cout<<testIgnoreCase(str1,str2);//1
    return 0;
}

上面的代码同一将两个字符串转换为了小写,然后比较。当然你先转换为大写也行。

看起来功能已经实现了。

但这种做法真的严谨吗?

考虑下面的两个例子:

//C++ example1

bool testIgnoreCase(string str1, string str2){
    transform(str1.begin(),str1.end(),str1.begin(),::tolower);
    transform(str2.begin(),str2.end(),str2.begin(),::tolower);

    //Or
    //transform(str1.begin(),str1.end(),str1.begin(),::toupper);
    //transform(str2.begin(),str2.end(),str2.begin(),::toupper);

    cout<<str1<<" "<<str2<<endl;//ı i
    return str1 == str2;
}

int main()
{
    string str1 = "ı";//unicode=305,注意不在ascii范围内
    string str2 = "I";//常见的大写字母I
    cout<<testIgnoreCase(str1,str2);//0
    return 0;
}
//C++ example2

bool testIgnoreCase(string str1, string str2){
    //transform(str1.begin(),str1.end(),str1.begin(),::tolower);
    //transform(str2.begin(),str2.end(),str2.begin(),::tolower);

    //Or
    transform(str1.begin(),str1.end(),str1.begin(),::toupper);
    transform(str2.begin(),str2.end(),str2.begin(),::toupper);

    cout<<str1<<" "<<str2<<endl;//İ I
    return str1 == str2;
}

int main()
{
    string str1 = "İ";//unicode=304,注意不在ascii范围内
    string str2 = "i";//常见的小写字母i
    cout<<testIgnoreCase(str1,str2);//0
    return 0;
}

从上面两个例子中,可以看到,不管是全部转换为小写还是全部转换为大写,再比较的方式,都是不严谨的。主要的原因是我们没有考虑超出ascii编码范围的字符。

上面的例子中,总共涉及到四个字符,分别为:

i

常见的小写字母i,Ascii=105

I

常见的大写字母I,Ascii=73

ı

unicode=305

İ

unicode=304

因此引出一个疑问:这四个字符,是一族的吗?换句话说,它们是否真的被视为等价?如果它们不等价,上面的问题就不算是问题了。

这个问题就涉及到两种语言之间的差异了:

Java中,它们之间大小写转换关系如下:

而C++中,这几个字符不被视为等价,这就意味着,就算你这样写(先转换为小写,如果还不相等,再转换为大写判断;当然先转换为大写后转换为小写是一样的思路):

//C++

bool testIgnoreCase(string str1, string str2){
    transform(str1.begin(),str1.end(),str1.begin(),::tolower);
    transform(str2.begin(),str2.end(),str2.begin(),::tolower);
    if(str1 == str2) {
        return true;
    }
    transform(str1.begin(),str1.end(),str1.begin(),::toupper);
    transform(str2.begin(),str2.end(),str2.begin(),::toupper);
    return str1 == str2;
}

也不会起丝毫作用。

那Java中是如何实现IgnoreCace的呢?

看Java中的equalsIgnoreCase()函数源码:

//Java
    
public boolean equalsIgnoreCase(String anotherString) {
    return (this == anotherString) ? true
            : (anotherString != null)
            && (anotherString.value.length == value.length)
            && regionMatches(true, 0, anotherString, 0, value.length);
}

public boolean regionMatches(boolean ignoreCase, int toffset,
        String other, int ooffset, int len) {
    char ta[] = value;
    int to = toffset;
    char pa[] = other.value;
    int po = ooffset;
    // Note: toffset, ooffset, or len might be near -1>>>1.
    if ((ooffset < 0) || (toffset < 0)
            || (toffset > (long)value.length - len)
            || (ooffset > (long)other.value.length - len)) {
        return false;
    }
    while (len-- > 0) {
        char c1 = ta[to++];
        char c2 = pa[po++];
        if (c1 == c2) {
            continue;
        }
        if (ignoreCase) {
            // If characters don't match but case may be ignored,
            // try converting both characters to uppercase.
            // If the results match, then the comparison scan should
            // continue.
            char u1 = Character.toUpperCase(c1);
            char u2 = Character.toUpperCase(c2);
            if (u1 == u2) {
                continue;
            }
            // Unfortunately, conversion to uppercase does not work properly
            // for the Georgian alphabet, which has strange rules about case
            // conversion.  So we need to make one last check before
            // exiting.
            if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {
                continue;
            }
        }
        return false;
    }
    return true;
}

可以看到,Java中的忽略大小写比较先将字符转换为大写,对于不相等的字符,又转换为小写比较;这样做相当于多了一层保障。

再细究,我们先看小写转换,观察其更为底层的实现:

 1 int toLowerCase(int ch) {
 2     int mapChar = ch;
 3     int val = getProperties(ch);
 4 
 5     if ((val & 0x00020000) != 0) {
 6         if ((val & 0x07FC0000) == 0x07FC0000) {
 7             switch(ch) {
 8                 // map the offset overflow chars
 9                 case 0x0130 : mapChar = 0x0069; break;
10                 case 0x2126 : mapChar = 0x03C9; break;
11                 case 0x212A : mapChar = 0x006B; break;
12                 case 0x212B : mapChar = 0x00E5; break;
13                 // map the titlecase chars with both a 1:M uppercase map
14                 // and a lowercase map
15                 case 0x1F88 : mapChar = 0x1F80; break;

16                 /*******为保证阅读效果,省略很多case*******/

17                 case 0xA7AA : mapChar = 0x0266; break;
18                 // default mapChar is already set, so no
19                 // need to redo it here.
20                 // default       : mapChar = ch;
21             }
22         }
23         else {
24             int offset = val << 5 >> (5+18);
25             mapChar = ch + offset;
26         }
27     }
28     return mapChar;
29 }  

源码中的getProperties,获取到字符的属性(感兴趣的可以阅读源码),然后根据不同的情况执行对应的操作。对于我们的例子,第9行

case 0x0130 : mapChar = 0x0069; break;

将İ(304)转换为i(105)。注意程序中是16进制的。

再看大写转换:

 1 int toUpperCase(int ch) {
 2     int mapChar = ch;
 3     int val = getProperties(ch);
 4 
 5     if ((val & 0x00010000) != 0) {
 6       if ((val & 0x07FC0000) == 0x07FC0000) {
 7         switch(ch) {
 8           // map chars with overflow offsets
 9         case 0x00B5 : mapChar = 0x039C; break;
10         case 0x017F : mapChar = 0x0053; break;
11         case 0x1FBE : mapChar = 0x0399; break;
12           // map char that have both a 1:1 and 1:M map
13         case 0x1F80 : mapChar = 0x1F88; break;

14         /*******为保证阅读效果,这里省略很多case*******/

15         case 0x2D2D : mapChar = 0x10CD; break;
16           // ch must have a 1:M case mapping, but we
17           // can't handle it here. Return ch.
18           // since mapChar is already set, no need
19           // to redo it here.
20           //default       : mapChar = ch;
21         }
22       }
23       else {
24         int offset = val  << 5 >> (5+18);
25         mapChar =  ch - offset;
26       }
27     }
28     return mapChar;
29 }

转换ı(305)时,程序跳到了第24行:

int offset = val  << 5 >> (5+18);

将其转换为I(73)。

至此,上面的例子可以正常运行了。

总结

对于Java:

     1. 对于Ascii码表中的字符,传统方法(只转换为大写或小写)完全没有问题;

     2. 若要考虑更多字符集,需多加考虑,这时要多加一次转换和比较。除了文中列举的字符,还有其他字符存在类似的问题。

对于C++:

     1. 对于Ascii码表中的字符,传统方法(只转换为大写或小写)完全没有问题;

     2. C++对于超出Ascii码表的字符处理方式和Java不同。由于看不到tolower的源码,这里没有进一步分析,有知晓的读者欢迎留言。

后记

1. 文中涉及到了“等价”和“相等”的概念,这里不做具体区分,可参考《Effective C++》详细了解。

2. C++还有其他函数如strcasecmp/stricmp可以忽略大小写比较,它们都是只转换为小写后比较,具体可以看官网说明:

XXX compares string1 and string2 without sensitivity to case. All alphabetic characters in the two arguments string1 and string2 are converted to lowercase before the comparison.

参考话题

https://stackoverflow.com/questions/15518731/understanding-logic-in-caseinsensitivecomparator

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏数据结构与算法

BZOJ3585: mex(主席树)

Description   有一个长度为n的数组{a1,a2,...,an}。m次询问,每次询问一个区间内最小没有出现过的自然数。 Input   第一行n,m...

4209
来自专栏海天一树

小朋友学C++(17):析构函数

析构函数(destructor) 与构造函数相反,当对象结束其生命周期时(例如对象所在的函数已调用完毕),系统自动执行析构函数。析构函数往往用来做“清理善后” ...

29110
来自专栏用户2442861的专栏

JAVA反射机制作用是什么

Java的反射机制是Java特性之一,反射机制是构建框架技术的基础所在。灵活掌握Java反射机制,对大家以后学习框架技术有很大的帮助。

8432
来自专栏Golang语言社区

Go语言语法汇总

最近看了看GoLang,把Go语言的语法总结了一下,做个快速参考 数据类型 ---- var varName type,var var1,var2… type,...

4328
来自专栏Crossin的编程教室

【Python 第66课】列表综合

大家假期过得可好?今天来讲讲 Python 里一个我非常喜欢的特性--列表综合(List Comprehension)。所谓列表综合,就是通过一个已有的列表生成...

3508
来自专栏小古哥的博客园

读书笔记《PHP与MySQL程序设计》一

第1章 PHP概述 1.1  历史(PHP4、PHP5、PHP5.3、PHP6[未发布]) 1.2 一般语言特性(实用性、强大功能、可选择性、成本[开源]) 第...

4016
来自专栏java一日一条

Java 函数调用是传值还是传引用?从字节码角度来看看 !

https://www.oschina.net/question/2507499_2244027,其中的变量a前后的输出是什么?

983
来自专栏大前端_Web

javascript高级程序设计(4-5)章笔记

版权声明:本文为吴孔云博客原创文章,转载请注明出处并带上链接,谢谢。 https://blog.csdn.net/wkyseo/articl...

1044
来自专栏一枝花算不算浪漫

[Java面试二]Java基础知识精华部分.

3979
来自专栏liulun

Nim教程【十三】

类型转换 Nim支持显示类型转换和隐式类型转换 使用casts操作符完成显示类型转换工作, 显示类型转换工作是编译期完成的工作,是位模式的 隐式类型转换也是编译...

2196

扫码关注云+社区

领取腾讯云代金券