提示
阅读本文需要同时对 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++ 中, 这几个字符不被视为等价, 这就意味着, 就算你这样写(先转换为小写, 如果还不相等, 再转换为大写判断; 当然先转换为大写后转换为小写是一样的思路):
- 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 //Java
- int toLowerCase(int ch) {
- int mapChar = ch;
- int val = getProperties(ch);
- if ((val & 0x00020000) != 0) {
- if ((val & 0x07FC0000) == 0x07FC0000) {
- switch(ch) {
- // map the offset overflow chars
- case 0x0130 : mapChar = 0x0069; break;
- case 0x2126 : mapChar = 0x03C9; break;
- case 0x212A : mapChar = 0x006B; break;
- case 0x212B : mapChar = 0x00E5; break;
- // map the titlecase chars with both a 1:M uppercase map
- // and a lowercase map
- case 0x1F88 : mapChar = 0x1F80; break;
- /******* 为保证阅读效果, 这里省略很多 case*******/
- case 0xA7AA : mapChar = 0x0266; break;
- // default mapChar is already set, so no
- // need to redo it here.
- // default : mapChar = ch;
- }
- }
- else {
- int offset = val <<5>> (5+18);
- mapChar = ch + offset;
- }
- }
- return mapChar;
- }
源码中的 getProperties, 获取到字符所处的属性集, 然后根据不同的情况执行对应的操作, 对于我们的例子, 第 11 行
case 0x0130 : mapChar = 0x0069; break;
将İ(304)转换为 i(105). 注意程序中是 16 进制的.
再看大写转换:
- int toUpperCase(int ch) {
- int mapChar = ch;
- int val = getProperties(ch);
- if ((val & 0x00010000) != 0) {
- if ((val & 0x07FC0000) == 0x07FC0000) {
- switch(ch) {
- // map chars with overflow offsets
- case 0x00B5 : mapChar = 0x039C; break;
- case 0x017F : mapChar = 0x0053; break;
- case 0x1FBE : mapChar = 0x0399; break;
- // map char that have both a 1:1 and 1:M map
- case 0x1F80 : mapChar = 0x1F88; break;
- /******* 为保证阅读效果, 这里省略很多 case*******/
- case 0x2D2D : mapChar = 0x10CD; break;
- // ch must have a 1:M case mapping, but we
- // can't handle it here. Return ch.
- // since mapChar is already set, no need
- // to redo it here.
- //default : mapChar = ch;
- }
- }
- else {
- int offset = val <<5>> (5+18);
- mapChar = ch - offset;
- }
- }
- return mapChar;
- }
转换ı(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://www.cnblogs.com/xiaoxi666/p/9535084.html