从std::string中提取(第一个)UTF-8字符

6
我需要使用一个C++实现PHP的mb_strtoupper函数来模仿维基百科的行为。
我的问题是,我只想向这个函数提供一个UTF-8字符,即std::string的第一个字符。
std::string s("äbcdefg");
mb_strtoupper(s[0]); // this obviously can't work with multi-byte characters
mb_strtoupper('ä'); // works

有没有一种高效的方法来检测/返回字符串中仅第一个UTF-8字符?


3
如果没有库可用,你最好编写一个函数来读取UTF-8字符串的第一个字符。方法是通过读取代理字节并将其组合成单个32位整数 - Unicode代码点 - 来实现。然后,这个整数就是你的第一个字符。例如,请参阅此答案:https://dev59.com/H07Sa4cB1Zd3GeqP1CVt#2953960 - MicroVirus
在字符上进行大小写转换(以及其他操作)是一个不好的想法。它会破坏组合字符和具有1:多映射的字符(例如'ß' => 'SS')的功能。 - Mihai Nita
完全同意Mihai的观点。然而,在这种情况下,我需要模仿维基百科的内部行为进行分析,因此这是我采取的方法。 - Adrian
2个回答

8
在UTF-8中,第一个字节的高位告诉您有多少个后续字节是同一代码点的一部分。
0b0xxxxxxx: this byte is the entire code point
0b10xxxxxx: this byte is a continuation byte - this shouldn't occur at the start of a string
0b110xxxxx: this byte plus the next (which must be a continuation byte) form the code point
0b1110xxxx: this byte plus the next two form the code point
0b11110xxx: this byte plus the next three form the code point

这种模式可能会持续下去,但我认为有效的UTF-8编码从未使用超过四个字节来表示单个代码点。

如果您编写一个函数来计算设置为1的前导位数的数量,则可以使用它来确定在输入为有效UTF-8的情况下拆分字节序列以隔离第一个逻辑代码点的位置。 如果您想要针对无效的UTF-8进行加固,您需要编写更多的代码。

另一种方法是利用连续字节始终匹配模式0b10xxxxxx的事实,因此您取第一个字节,然后只要下一个字节匹配该模式,就继续取字节。

std::size_t GetFirst(const std::string &text) {
  if (text.empty()) return 0;
  std::size_t length = 1;
  while ((text[length] & 0b11000000) == 0b10000000) {
    ++length;
  }
  return length;
}

对于许多语言而言,一个码点通常映射到一个字符。但是人们所认为的单个字符可能更接近于Unicode所称的字形簇,即一种或多种代码点组合在一起以产生字形。
在您的示例中,ä可以用不同的方式表示:它可以是单个码点U+00E4 LATIN SMALL LETTER A WITH DIAERESIS,也可以是U+0061 LATIN SMALL LETTER A和U+0308 COMBINING DIAERESIS的组合。幸运的是,只选择第一个码点应该适用于您将首字母大写的目标。
如果您真的需要第一个字形簇,则必须查看第一个码点以外的内容,以查看下一个码点是否与其组合。对于许多语言,仅需知道哪些码点是“非间距”、“组合”或变体选择器即可。对于某些复杂的脚本(例如,韩文?),您可能需要参考此Unicode Consortium technical report

1

str.h

#include <iostream>
#include "str.h"

int main (){
    std::string text = "äbcdefg";
    std::string str = str::substr(text, 0, 1); // Return:~ ä
    std::cout << str << std::endl;
}

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