zoukankan      html  css  js  c++  java
  • 你写的字符(串)忽略大小写比较函数真的严谨吗?

    提示

    阅读本文需要同时对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

    好用的工具网站

    字符码在线查询 https://www.litefeel.com/tools/ascii.php 

  • 相关阅读:
    MySQL日志
    MySQL索引和事务
    【收集】腾讯AlloyTeam
    js基础知识点(只有点)
    【扩展】Canvas绘制列表的尝试
    开播 开博 凯博
    【总结】移动web问题小结
    〖前端开发〗HTML/CSS基础知识学习笔记
    第四次读书笔记——《代码大全》(续)
    C++笔记(1)
  • 原文地址:https://www.cnblogs.com/xiaoxi666/p/9535084.html
Copyright © 2011-2022 走看看