C/C++ UTF-8 大小写转换

17

问题: 有一个方法和对应的测试用例,在一台机器上可以运行,但在另一台机器上失败了(详见下文)。我认为代码有问题,在一台机器上偶然成功。不幸的是,我找不到问题所在。

请注意,我无法真正影响使用std::string和utf-8编码的需求。使用C ++方法完全没问题,但不幸的是我找不到任何东西。因此使用C函数。

该方法:

std::string firstCharToUpperUtf8(const string& orig) {
  std::string retVal;
  retVal.reserve(orig.size());
  std::mbstate_t state = std::mbstate_t();
  char buf[MB_CUR_MAX + 1];
  size_t i = 0;
  if (orig.size() > 0) {
    if (orig[i] > 0) {
      retVal += toupper(orig[i]);
      ++i;
    } else {
      wchar_t wChar;
      int len = mbrtowc(&wChar, &orig[i], MB_CUR_MAX, &state);
      // If this assertion fails, there is an invalid multi-byte character.
      // However, this usually means that the locale is not utf8.
      // Note that the default locale is always C. Main classes need to set them
      // To utf8, even if the system's default is utf8 already.
      assert(len > 0 && len <= static_cast<int>(MB_CUR_MAX));
      i += len;
      int ret = wcrtomb(buf, towupper(wChar), &state);
      assert(ret > 0 && ret <= static_cast<int>(MB_CUR_MAX));
      buf[ret] = 0;
      retVal += buf;
    }
  }
  for (; i < orig.size(); ++i) {
    retVal += orig[i];
  }
  return retVal;
}

测试:

TEST(StringUtilsTest, firstCharToUpperUtf8) {
  setlocale(LC_CTYPE, "en_US.utf8");
  ASSERT_EQ("Foo", firstCharToUpperUtf8("foo"));
  ASSERT_EQ("Foo", firstCharToUpperUtf8("Foo"));
  ASSERT_EQ("#foo", firstCharToUpperUtf8("#foo"));
  ASSERT_EQ("ßfoo", firstCharToUpperUtf8("ßfoo"));
  ASSERT_EQ("Éfoo", firstCharToUpperUtf8("éfoo"));
  ASSERT_EQ("Éfoo", firstCharToUpperUtf8("Éfoo"));
}

测试失败(仅发生在两台计算机中的一台上):

Failure
Value of: firstCharToUpperUtf8("ßfoo")
  Actual: "\xE1\xBA\x9E" "foo"
Expected: "ßfoo"

这两台机器都安装了en_US.utf8语言环境。但它们使用的libc版本不同。无论在哪台机器上编译,代码都可以在使用GLIBC_2.14的机器上正常运行,但在另一台机器上无法正常运行,只能在该机器上编译,否则就会缺少适当的libc版本。

无论如何,在一个机器上编译并运行此代码是正确的,而在另一个机器上则失败了。代码肯定有问题,我想知道是什么问题。指出C++方法(特别是STL)也很好。由于其他外部要求,应避免使用Boost和其他库。


+1 针对问题的清晰描述。 - 0x6B6F77616C74
3
在Unicode中,如果你一次只操作单个码点,那么你做错了。转换操作只对范围有意义。 - JoeG
1
小写的尖 s:ß;大写的尖 s:ẞ。你在 assert 中使用了大写版本吗?似乎 glibg 2.14 遵循回溯点的观点(Unicode5.1之前没有大写版本),而另一台机器上的 libc 使用 Unicode 5.1 ẞ=U1E9E... - Kwariz
@Joe Gauterin: 我不这么做。我会查看可能是Unicode的内容的第一个字符,如果它不能降级为ASCII,则使用范围进行处理,因此需要使用len。 - b.buchhold
1
虽然我总体上很喜欢这个解决方案,但应该将 orig[i] > 0 替换为 (orig[i] & (1 << 7)) == 0,因为原始测试在 char 为无符号的系统(例如 ARM 上的 Linux)上不起作用。 - Niklas Schnelle
显示剩余2条评论
5个回答

10

也许有人会使用它(也许是用于测试)

通过这个,你可以制作简单的转换器:) 不需要额外的库 :)

http://pastebin.com/fuw4Uizk

1482个字母

示例

Ь <> ь
Э <> э
Ю <> ю
Я <> я
Ѡ <> ѡ
Ѣ <> ѣ
Ѥ <> ѥ
Ѧ <> ѧ
Ѩ <> ѩ
Ѫ <> ѫ
Ѭ <> ѭ
Ѯ <> ѯ
Ѱ <> ѱ
Ѳ <> ѳ
Ѵ <> ѵ
Ѷ <> ѷ
Ѹ <> ѹ
Ѻ <> ѻ
Ѽ <> ѽ
Ѿ <> ѿ
Ҁ <> ҁ
Ҋ <> ҋ
Ҍ <> ҍ
Ҏ <> ҏ
Ґ <> ґ
Ғ <> ғ
Ҕ <> ҕ
Җ <> җ
Ҙ <> ҙ
Қ <> қ
Ҝ <> ҝ
Ҟ <> ҟ
Ҡ <> ҡ
Ң <> ң

你还记得你是怎么制作这个列表的吗?我试图利用它,但是一些健全性检查失败了(未排序、重复),所以它可能在传输到 pastebin、我的浏览器、我的剪贴板或我的 IDE 的过程中受损了。现在我正在尝试将其转换为十六进制的 char32_t - Cygon
@Cygon,我想我找到了一些在线列表,并手动整理了一下。另一个解决方案是使用Python并自己打印此类列表。例如 print("ĄŻŹĆ".lower()) - Gelldur

5
以下的C++11代码对我来说是有效的(暂时忽略如何翻译尖端字母的问题——它没有改变。它正在逐渐被德语淘汰)。
优化和仅大写第一个字母将留作练习。
编辑:正如指出的,codecvt似乎已经被弃用了。然而,它应该保留在标准中,直到有合适的替代品定义。请参考Deprecated header <codecvt> replacement
#include <codecvt>
#include <iostream>
#include <locale>

std::locale const utf8("en_US.UTF-8");

// Convert UTF-8 byte string to wstring
std::wstring to_wstring(std::string const& s) {
  std::wstring_convert<std::codecvt_utf8<wchar_t> > conv;
  return conv.from_bytes(s);
}

// Convert wstring to UTF-8 byte string
std::string to_string(std::wstring const& s) {
  std::wstring_convert<std::codecvt_utf8<wchar_t> > conv;
  return conv.to_bytes(s);
}

// Converts a UTF-8 encoded string to upper case
std::string tou(std::string const& s) {
  auto ss = to_wstring(s);
  for (auto& c : ss) {
    c = std::toupper(c, utf8);
  }
  return to_string(ss);
}

void test_utf8(std::ostream& os) {
  os << tou("foo" ) << std::endl;
  os << tou("#foo") << std::endl;
  os << tou("ßfoo") << std::endl;
  os << tou("Éfoo") << std::endl;
}    

int main() {
  test_utf8(std::cout);
}

1
顺便说一下,德语中没有以尖音S开头的单词。 - Gerhard Wesp
请注意,自C++17以来,codecvt已被弃用。 - usernameiwantedwasalreadytaken

1
你期望德语 ß 字符的大写版本在该测试用例中是什么?
换句话说,您的基本假设是错误的。
请注意,注释中的维基百科说明:
“尖s在拉丁字母中几乎是独一无二的,因为它没有传统的大写形式(少数其他例子之一是kra,ĸ,它在格陵兰语中使用)。这是因为它从不出现在德语文本的开头,而传统的德语印刷(使用黑体字)从不使用全大写。当使用全大写时,当前的拼写规则要求用SS代替ß。但是,在2010年,当在所有大写字母中书写地理名称时,其使用成为强制性。”
因此,具有尖s作为首字母的基本测试用例违反了德国的规则。我仍然认为我有一个观点,即原始帖子的前提是错误的,字符串不能在所有语言中通常自由转换为大写和小写。

@KerrekSB 感谢您提供的参考资料,我添加了一些引用文本,我觉得这些文本可以加强我的论点... - unwind
2
这只是一个不必要的分散注意力的例子。使用希伯来语、阿拉伯语、中文或任何印度文字系统作为一个例子会更简单,因为在这些语言中没有大小写之分。 - Kerrek SB
它应该保持不变,完全符合预期的测试和#foo的情况。这是根据towupper的手册。不幸的是,许多字符串违反了语言规则。如果您想要示例,请考虑乐队名称、电影或维基百科页面标题:http://en.wikipedia.org/wiki/%C3%9F 字符串可能以此字符开头(与德语单词不同),应通过保留初始字符来转换。 - b.buchhold
@KerrekSB: 非常感谢。我也不知道这个字符存在,看起来它是我的问题来源... - b.buchhold
1
土耳其语中也有无点的i和带点的I。因此,根据语境 i<->I 与 I<->ı、i<->İ 一样可能是正确或错误的。 - Alexey Frunze
@b.buchhold:是的,确实是U+1E9E,这就是你似乎得到的。每个人都赢了。 - Kerrek SB

1

小写的尖 s:ß;大写的尖 s:ẞ。你在断言中使用了大写版本吗? 看起来 glibg 2.14 遵循实现 pre unicode5.1 没有尖 s 的大写版本,而另一台机器上的 libc 使用 unicode 5.1 ẞ=U1E9E...


7
这是错误的。许多代码点在大小写之间具有一对一的映射关系。你必须对字符串进行大小写映射,而不是单个字符,否则你的结果会很糟糕。 U+00DF的正确大写形式是“SS”。它不是U+1E9E!!请参考UCD。 - tchrist
@tchrist 错了吗?嗯,至少这取决于设计师和用户的观点。U+1E9E 在 Unicode 分类中属于字母和大写字母类别,将其称为小写版本的 U+00DF。这是否反映了德语中的普遍用法,我真的不知道,但在阅读了此博客上找到的评论后,我有所怀疑。但你是对的,由于它没有被广泛使用,以一个以 sharp s 开头的单词的正确大写形式应该是 SS(如果您问德国排版师则是 SZ)... - Kwariz
1
我怀疑在未来的几年里,大多数计算机用户都会希望/期望 成为 ß 的大写形式,并认为一对二的映射方式令人讨厌和过时... - R.. GitHub STOP HELPING ICE
4
@tchrist: 在德文中,使用“SS”将U+00DF大写为“IN MASSEN”(大量)并不适用于表示“in Maßen”(适度、有限的)。这是因为“Maßen”和“Massen”是不同的词汇,甚至是相反的意思。同样,“Maße”(尺寸)和“Masse”(质量)也是如此。 - Secure
尽管“ß”的大小写问题可能很复杂且有争议,但我的问题围绕不同机器上的不同行为而展开。由于Unicode标准会受到更改,而glibg版本必须决定采用哪个版本,因此这正好回答了我的问题。这就是为什么我欣然接受了这个答案的原因。 - b.buchhold
显示剩余2条评论

0

问题在于您的语言环境不符合规范,那些触发断言的语言环境是不符合规范的。

技术报告 N897 在 B.1.2[LC_CTYPE 解释] 中要求:

由于 LC_CTYPE 字符类基于 C 标准字符类定义,因此该类别不支持多字符元素。例如,德语字符 ß 传统上被归类为小写字母。没有相应的大写字母;在正确的德语文本大写中,ß 将被替换为 SS;即两个字符。这种转换超出了 touppertolower 关键字的范围。

该技术报告于 2001 年 12 月 25 日发布。但根据:https://en.wikipedia.org/wiki/Capital_%E1%BA%9E

在2010年,德国官方文件规定,在所有大写地理名称中使用大写字母ẞ成为了强制性要求。
但是,标准委员会尚未重新审视这个问题,因此从技术上讲,无论德国政府说什么,toupper的标准化行为都应该不对ß字符进行任何更改。
这种不一致的原因是setlocale
安装指定的系统区域设置或其部分作为新的C语言环境。
因此,非兼容的系统区域设置en_US.utf8指示toupper修改ß字符。不幸的是,专业化ctype<char>::clasic_tablectype<wchar_t>上不可用,因此您无法修改其行为。留给您两个选择:
  1. 创建一个 const map<wchar_t, wchar_t>,用于将每个可能的小写 wchar_t 转换为相应的大写 wchar_t
  2. 增加如下检查是否有 L'ß':

    int ret = wcrtomb(buf, wChar == L'ß' ? L'ẞ' : towupper(wChar), &state);
    

实时示例


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接