Unicode:如何让用户东方不败和[Family: Man, Woman, Girl, Boy]顺利通过用户名长度检查?

给你这样一个需求:实现一个函数,我们将用这个函数来计算用户在注册账号时输入的用户名长度,当用户名长度超过限制5时,我们会阻止用户使用这个用户名。函数输入为UTF-8编码的std::string

这听起来似乎很容易,一行代码就能够搞定:

cpp 复制代码
int stringLength(const std::string& s) { return s.size(); }

注册我们账号的第一位用户叫做Mary,stringLength不负众望地给我们返回了4,这让她通过了用户名长度检测,顺利注册了账号。

第二位用户叫做东方不败,他的名字看起来也只有4个字,应该也能够顺利通过用户名长度检测。然而事与愿违,用户界面提示用户名超长,通过一番调试,我们发现stringLength返回了12。

虽然初看起来有点奇怪,但是作为一个程序员,经过短暂的思考以后,我们不难弄清楚,一个英文字母和一个汉字虽然都对应一个Unicode code point,但是在使用UTF-8编码时,英文字母和汉字由于code point的范围不同,它们的"地位"是不同的。

First code point Last code point Byte 1 Byte 2 Byte 3 Byte 4
U+0000 U+007F 0yyyzzzz
U+0080 U+07FF 110xxxyy 10yyzzzz
U+0800 U+FFFF 1110wwww 10xxxxyy 10yyzzzz
U+010000 U+10FFFF 11110uvv 10vvwwww 10xxxxyy 10yyzzzz

之所以会得到12,是因为常用汉字的Unicode Block "CJK Unified Ideographs" code point范围为U+4E00~U+9FFF,使用UTF-8编码时占3个字节,3x4=12:

复制代码
东方不败
U+4E1C U+65B9 U+4E0D U+8D25
0xE4 0xB8 0x9C 0xE6 0x96 0xB9 0xE4 0xB8 0x8D 0xE8 0xB4 0xA5

但是作为一名对Unicode/UTF-8一窍不通的普通用户,东方不败大概不会满意我们的解释,他只知道自己心爱的名字"东方不败"4个字因为超出长度限制5而无法注册,这太荒谬了。为了让这名用户满意,我们需要实现一个stringLength_v2来纠正这个问题。

stringLength_v2中,std::string::size将派不上用场,它代表的是字符串占用了几个字节,而不是里面有几个文字。由于C++标准库中并没有可靠的Unicode编解码接口,此时我们最好依赖三方库,使用icu将UTF-8编码的std::string转换成icu::UnicodeString,然后"数一数"里面到底有几个code point,按我们目前的理解,一个code point就是一个字,有几个code point就是有几个字。

cpp 复制代码
int stringLength_v2(const std::string& s) {
  icu::UnicodeString ustr = icu::UnicodeString::fromUTF8(s);
  int32_t length = 0;
  for (int32_t i = 0; i < ustr.length();) {
    UChar32 c;
    U16_NEXT(ustr.getBuffer(), i, ustr.length(), c);
    if (c >= 0) {
      ++length;
    }
  }
  return length;
}

需要注意的是,icu::UnicodeString内部使用UTF-16存储数据,因此ustr.length()并不是code point的个数,我们必须自己来数。

将"东方不败"4个字传给stringLength_v2以后,我们如愿得到了4,用户东方不败也顺利注册了自己的账号。

又来了一位用户,他的名字叫做 👨‍👩‍👧‍👦,你不要觉得奇怪,这个世界发展很快,也许以后每个人都会把emoji放进自己的名字,毕竟emoji也是Unicode,而且还是很酷的Unicode。这个用户名看起来只有一个"字",但他也没能顺利地通过用户名长度检测,我们惊奇地发现,stringLength_v2给我们返回了7。

这次又是哪里弄错了呢?stringLength_v2数出来有7个code point,但是这个用户的名字看起来只有一个"字"。把7个code point都打印出来,它们分别是:

复制代码
👨‍👩‍👧‍👦
U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466

他的名字确实有7个code point,但显示时又只占据了一个字符的空间。经过一番学习,我们又了解到了grapheme cluster和combining character的概念,简而言之,用户看到的一个"字",并不对应着一个code point,而是对应着一个grapheme cluster。而一个grapheme cluster可能是由多个code point组成的。除了emoji,还有下面这些例子,它们都是包含combining character的grapheme cluster,我们可以使用C++的\u转义来构造它们:

复制代码
"\u092b\u093f" -> फि
"\u0061\u0308\u0303\u0323\u032d" -> ạ̭̈̃
"\u0e02\u0e36\u0e49" -> ขึ้

为了让用户👨‍👩‍👧‍👦能够顺利注册账号,我们需要再实现一个stringLength_v3,这个版本不再计算code point的个数,而是计算grapheme cluster的个数,它才是用户看到的字符的个数。

cpp 复制代码
int stringLength_v3(const std::string& s) {
  UErrorCode status = U_ZERO_ERROR;
  icu::UnicodeString ustr = icu::UnicodeString::fromUTF8(s);
  std::unique_ptr<icu::BreakIterator> bi(
      icu::BreakIterator::createCharacterInstance(icu::Locale::getDefault(),
                                                  status));
  if (U_FAILURE(status) || !bi) {
    return -1;
  }

  bi->setText(ustr);
  int count = 0;
  for (int32_t start = bi->first(), end = bi->next();
       end != icu::BreakIterator::DONE; start = end, end = bi->next()) {
    ++count;
  }
  return count;
}

stringLength_v3的帮助下,用户👨‍👩‍👧‍👦成功注册了账号,他对stringLength_v3表示了感谢。不过需要注意的是,Unicode并没有规定一个grapheme cluster中code point数量的上限,恶意用户完全可以构造一个超长的单一grapheme cluster来消耗我们宝贵的资源,因此有必要为s.size()也加上一个限制,比如说64。如果真的有善良的用户通过了stringLength_v3考验,但是没能通过s.size()的考验,我们可以考虑把他介绍给我们的竞争对手,他们拥有丰富的资源。

stringLength_v3让所有用户顺利注册了账号,皆大欢喜的结局!


一些有用的URL:

  1. https://www.compart.com/en/unicode/plane/U+0000: 拥有整套的Unicode数据集,可以让你获取到每个Unicode code point你想了解的所有信息
  2. https://www.compart.com/en/unicode/block/U+4E00: Unicode Block "CJK Unified Ideographs"
  3. https://home.unicode.org/: Unicode官网
  4. https://www.unicode.org/versions/latest/: Unicode标准最新版本
  5. https://unicode.org/emoji/charts/full-emoji-list.html: Emoji全集
  6. https://icu.unicode.org/: icu官网
  7. https://en.wikipedia.org/wiki/Unicode: Unicode维基百科
  8. https://en.wikipedia.org/wiki/UTF-8: UTF-8维基百科

最后附上完整的代码和输出。

代码

cpp 复制代码
#include <unicode/brkiter.h>
#include <unicode/locid.h>
#include <unicode/unistr.h>
#include <unicode/utypes.h>

#include <iostream>
#include <string>
#include <vector>

int stringLength(const std::string& s) { return s.size(); }

int stringLength_v2(const std::string& s) {
  icu::UnicodeString ustr = icu::UnicodeString::fromUTF8(s);
  int32_t length = 0;
  for (int32_t i = 0; i < ustr.length();) {
    UChar32 c;
    U16_NEXT(ustr.getBuffer(), i, ustr.length(), c);
    if (c >= 0) {
      ++length;
    }
  }
  return length;
}

int stringLength_v3(const std::string& s) {
  UErrorCode status = U_ZERO_ERROR;
  icu::UnicodeString ustr = icu::UnicodeString::fromUTF8(s);
  std::unique_ptr<icu::BreakIterator> bi(
      icu::BreakIterator::createCharacterInstance(icu::Locale::getDefault(),
                                                  status));
  if (U_FAILURE(status) || !bi) {
    return -1;
  }

  bi->setText(ustr);
  int count = 0;
  for (int32_t start = bi->first(), end = bi->next();
       end != icu::BreakIterator::DONE; start = end, end = bi->next()) {
    ++count;
  }
  return count;
}

int main() {
  std::vector<std::string> names = {"Mary",
                                    "东方不败",
                                    "👨‍👩‍👧‍👦",
                                    "\u0061\u0308\u0303\u0323\u032d",
                                    "\u0e02\u0e36\u0e49",
                                    "\u092b\u093f"};

  for (const std::string& name : names) {
    printf("%2d %2d %2d %s\n", stringLength(name), stringLength_v2(name),
           stringLength_v3(name), name.c_str());
  }

  return 0;
}

编译

bash 复制代码
g++ -licuuc main.cpp

输出

复制代码
 4  4  4 Mary
12  4  4 东方不败
25  7  1 👨‍👩‍👧‍👦
 9  5  1 ạ̭̈̃
 9  3  1 ขึ้
 6  2  1 फि
相关推荐
liulun18 分钟前
玩转 Skia 的颜色
c++
阿伍.27 分钟前
【指针】(适合考研、专升本)
c++·考研·c#
了不起的杰30 分钟前
[C++][设计模式] : 单例模式(饿汉和懒汉)
c++·单例模式·设计模式
1白天的黑夜142 分钟前
二叉树-226.翻转链表-力扣(LeetCode)
数据结构·c++·leetcode
秋风&萧瑟1 小时前
【C++】多重继承与虚继承
开发语言·c++
说码解字1 小时前
C++ 实现环形缓冲区
开发语言·c++
go_bai1 小时前
map与set的模拟实现
开发语言·c++·笔记·学习方法
秋风&萧瑟1 小时前
【C++】继承和派生
开发语言·c++
黑听人2 小时前
【力扣 中等 C++】90. 子集 II
开发语言·数据结构·c++·算法·leetcode
虾球xz2 小时前
CppCon 2015 学习:Algorithmic Differentiation C++ & Extremum Estimation
开发语言·c++·学习