你寫的字符(串)忽略大小寫比較函數真的嚴謹嗎?


提示

閱讀本文需要同時對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 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM