LoginSignup
19

More than 5 years have passed since last update.

UTF-8の文字列の文字数を数える

Last updated at Posted at 2014-07-26

UTF-8をコードポイントに分解する

まずはじめに、コマンドライン引数で入力された文字列を、コードポイントごとに表示するプログラムを作成します。
UTF-8からコードポイントを求める方法は、Wikipedia で説明されているとおりです。

注意: 以下の説明は、Windowsでは動作しません。Linux, FreeBSD, MacOSX (またはCygwin)で試してください。

#include <stdio.h>

/**
 * next_codepoint(&ptr);
 * ptrを、次のコードポイントの位置に動かす
 */
static int next_codepoint(const char **pp)
{
  const char *p = *pp;
  int c = -1;

  if (*p == 0) {
  } else if ((*p & 0x80) == 0) {
    c = *p++ & 0xFF;
  } else if ((*p & 0xE0) == 0xC0) {
    c = *p++ & 0x1F;
    c = (c << 6) | (*p++ & 0x3F);
  } else if ((*p & 0xF0) == 0xE0) {
    c = *p++ & 0x0F;
    c = (c << 6) | (*p++ & 0x3F);
    c = (c << 6) | (*p++ & 0x3F);
  } else if ((*p & 0xF8) == 0xF0) {
    c = *p++ & 0x07;
    c = (c << 6) | (*p++ & 0x3F);
    c = (c << 6) | (*p++ & 0x3F);
    c = (c << 6) | (*p++ & 0x3F);
  }
  *pp = p;
  return c;
}
int main(int argc, char **argv)
{
  if (argc >= 2) {
    const char *p = argv[1];
    int ch;
    int count = 0;

    while ((ch = next_codepoint(&p)) != -1) {
      printf("U+%04X ", ch);
      count++;
    }
    printf("\nCount = %d\n", count);
  }
  return 0;
}

実行結果は以下のとおりです。「あいうえお」はコードポイント5つから構成されています。

$ ./a.out あいうえお
U+3042 U+3044 U+3046 U+3048 U+304A 
Count = 5

グラハム書記素について

例えば、次の文字列の文字数を数えてみましょう。
ポイント
そのままコピー&ペーストして、実行してみると、4文字と判定されます。

$ ./a.out ポイント
U+30DD U+30A4 U+30F3 U+30C8 
Count = 4

では、次の文字列ではどうなるでしょうか。
ポイント
そのままコピー&ペーストして、実行してみると、5文字と判定されます。

$ ./a.out ポイント
U+30DB U+309A U+30A4 U+30F3 U+30C8 
Count = 5

後者は、「ポ」が正規分解済みなので、コードポイント数は2になりますが、文字数は1になります。
以下の説明では、Unicodeで単一のコードが与えられている文字等を「コードポイント」、一般的に1文字と認識する単位(厳密に言うとグラハム書記素)を「書記素」と表します。

Unicode.orgのデータを利用する

書記素を数えるには、テーブルを参照して書記素の区切り位置を探すしかありません。
現在、最新版のUnicode 7.0.0のGraphemeBreakPropertyが利用できるので、これを利用します。

ftp://ftp.unicode.org/Public/7.0.0/ucd/auxiliary/GraphemeBreakProperty.txt

このテキストファイルは、以下のように書かれています。
(#以降はコメントです)

000A          ; LF # Cc       <control-000A>

U+000Aは、LFに分類されるという意味です。

3099..309A    ; Extend # Mn   [2] COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK..COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK

U+3099からU+309Aは、Extendに分類されるという意味です。
これらの範囲に含まれないコードポイントは、Otherに分類されます。

以下のサンプルコードを動かすには、GraphemeBreakProperty.txtからgrapheme.datというバイナリファイルを作成する必要がありますので、頑張って作ってください。
grapheme.datは、例えばU+309AがExtendの場合、先頭から0x309Aバイト目が4になります。

#include <stdio.h>
#include <stdlib.h>

enum {
  CR = 1,
  LF = 2,
  Control = 3,
  Extend = 4,
  SpacingMark = 5,
  L = 6,
  V = 7,
  T = 8,
  LV = 9,
  LVT = 10,
  Regional_Indicator = 11,
};
enum {
  MAX_CODEPOINT = 0x10000,
};

/**
 * next_codepoint(&ptr);
 * ptrを、次のコードポイントの位置に動かす
 */
static int next_codepoint(const char **pp)
{
  const char *p = *pp;
  int c = -1;

  if (*p == 0) {
  } else if ((*p & 0x80) == 0) {
    c = *p++ & 0xFF;
  } else if ((*p & 0xE0) == 0xC0) {
    c = *p++ & 0x1F;
    c = (c << 6) | (*p++ & 0x3F);
  } else if ((*p & 0xF0) == 0xE0) {
    c = *p++ & 0x0F;
    c = (c << 6) | (*p++ & 0x3F);
    c = (c << 6) | (*p++ & 0x3F);
  } else if ((*p & 0xF8) == 0xF0) {
    c = *p++ & 0x07;
    c = (c << 6) | (*p++ & 0x3F);
    c = (c << 6) | (*p++ & 0x3F);
    c = (c << 6) | (*p++ & 0x3F);
  }
  *pp = p;
  return c;
}
char *load_grapheme_data(void)
{
  FILE *fp = fopen("grapheme.dat", "rb");
  char *dat = malloc(MAX_CODEPOINT);
  fread(dat, 1, MAX_CODEPOINT, fp);
  fclose(fp);
  return dat;
}
int main(int argc, char **argv)
{
  if (argc >= 2) {
    const char *p = argv[1];
    int ch;
    char *dat = load_grapheme_data();

    while ((ch = next_codepoint(&p)) != -1) {
      if (ch < MAX_CODEPOINT) {
        switch (dat[ch]) {
        case CR:
          printf("CR ");
          break;
        case LF:
          printf("LF ");
          break;
        case Control:
          printf("Control ");
          break;
        case Extend:
          printf("Extend ");
          break;
        case SpacingMark:
          printf("SpacingMark ");
          break;
        case L:
          printf("L ");
          break;
        case V:
          printf("V ");
          break;
        case T:
          printf("T ");
          break;
        case LV:
          printf("LV ");
          break;
        case LVT:
          printf("LVT ");
          break;
        default:
          printf("Other ");
          break;
        }
      }
    }
    printf("\n");
  }
  return 0;
}

実行結果は、以下のようになります。

$ ./a.out ポイント
Other Extend Other Other Other 

GraphemeBreakPropertyから区切り位置を探す

ここまで来たらもう一息です。
Grapheme Break Chart の表を見ると、区切り位置がどこかがわかります。
以下の表は、一部を抜粋したものです。

Screenshot.png

表の縦の列が前のコードポイント、横の列が後ろのコードポイントを表しています。「÷」が書記素の区切り、「×」が書記素の区切りではないことを表しています。
例えば、前のコードポイントがExtend、後ろのコードポイントがOtherなら、「÷」となるので書記素が区切られますが、前のコードポイントがOther、後ろのコードポイントがExtendなら、「×」となるので書記素が区切られません。(前のコードポイントと合わせて一つの書記素を作る)

/*
 * main関数以外は、変更していないので省略
 */
int main(int argc, char **argv)
{
  /**
   * http://unicode.org/Public/UNIDATA/auxiliary/GraphemeBreakTest.html
   */
  static const unsigned short grapheme_break[] = {
    0x0030,
    0x0004,
    0x0000,
    0x0000,
    0x0030,
    0x0030,
    0x06f0,
    0x01b0,
    0x0130,
    0x01b0,
    0x0130,
    0x0830,
  };
  if (argc >= 2) {
    const char *p = argv[1];
    int ch, type, prev_type = -1;
    char *dat = load_grapheme_data();
    int count = 0;

    while ((ch = next_codepoint(&p)) != -1) {
      if (ch < MAX_CODEPOINT) {
        type = dat[ch];

        if (prev_type >= 0) {
          if ((grapheme_break[prev_type] & (1 << type)) == 0) {
            printf("| ");
            count++;
          }
        }
        prev_type = type;

        switch (type) {
        case CR:
          printf("CR ");
          break;
        case LF:
          printf("LF ");
          break;
        case Control:
          printf("Control ");
          break;
        case Extend:
          printf("Extend ");
          break;
        case SpacingMark:
          printf("SpacingMark ");
          break;
        case L:
          printf("L ");
          break;
        case V:
          printf("V ");
          break;
        case T:
          printf("T ");
          break;
        case LV:
          printf("LV ");
          break;
        case LVT:
          printf("LVT ");
          break;
        default:
          printf("Other ");
          break;
        }
      }
    }
    if (prev_type >= 0) {
      count++;
    }
    printf("\nCount = %d\n", count);
  }
  return 0;
}

実行結果は、以下のようになります。書記素ごとに | で区切られます。

$ ./a.out ポイント
Other Extend | Other | Other | Other 
Count = 4

最後に

自前で作るより、ICUとかを使いましょう。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19