Help us understand the problem. What is going on with this article?

mb_strwidth の代わりに IntlChar を使って東アジアの文字幅を求める

More than 3 years have passed since last update.

PHP 7.0 で intl エクステンションに導入された IntlChar を使って東アジアの文字幅を求める関数を定義しました。

IntlChar について

IntlChar は Unicode Character Database で配布されているデータを文字の種類の判定に使います。東アジアの文字幅の判定には EastAsianWidth.txt が使われます。

IntlChar の実装には ICU の uchar.h が使われています。

ほかの言語の実装

Java の場合、ICU4J の UCharacter.getIntPropertyValue(codePoint, UProperty.EAST_ASIAN_WIDTH) を使います。Python の場合、標準モジュールの unicodedata.east_asian_width を使います。Go の場合、golang.org/x/text/width があります。使い方は「Go で UTF-8 の文字列を扱う の記事をご参照ください。

文字幅の種類を調べる

IntlChar::getIntPropertyValue の第2引数に IntlChar::PROPERTY_EAST_ASIAN_WIDTH を指定します。

var_dump(
    IntlChar::EA_FULLWIDTH === IntlChar::getIntPropertyValue("K", IntlChar::PROPERTY_EAST_ASIAN_WIDTH),
    IntlChar::EA_HALFWIDTH === IntlChar::getIntPropertyValue("カ", IntlChar::PROPERTY_EAST_ASIAN_WIDTH),
    IntlChar::EA_WIDE === IntlChar::getIntPropertyValue("カ", IntlChar::PROPERTY_EAST_ASIAN_WIDTH),
    IntlChar::EA_NARROW === IntlChar::getIntPropertyValue("a", IntlChar::PROPERTY_EAST_ASIAN_WIDTH),
    IntlChar::EA_AMBIGUOUS === IntlChar::getIntPropertyValue("α", IntlChar::PROPERTY_EAST_ASIAN_WIDTH),
    IntlChar::EA_NEUTRAL === IntlChar::getIntPropertyValue("À", IntlChar::PROPERTY_EAST_ASIAN_WIDTH)
);

関数を定義する

関数を定義してみましょう。UAX #11 の推奨によれば、東アジアの文字エンコーディングでは Ambiguous に属する文字は全角として扱い、東アジアではない文字エンコーディングでは Ambiguous を半角として扱うとのことです (via 「文字コード地獄秘話 第1話:Unicodeにおける全角・半角」)。コードポイント単位で文字を取り出すには grapheme_extract の第3引数に GRAPHEME_EXTR_MAXCHARS を指定します。

function intl_east_asian_width($str, $east = true)
{
    $size = strlen($str);
    $list = [IntlChar::EA_FULLWIDTH, IntlChar::EA_WIDE];

    if ($east) {
        $list[] = IntlChar::EA_AMBIGUOUS;
    }

    $width = 0;
    $current = 0;
    $next = 0;

    while ($next < $size) {
        $c = grapheme_extract($str, 1, GRAPHEME_EXTR_MAXCHARS, $current, $next);
        $current = $next;

        $v = IntlChar::getIntPropertyValue($c, IntlChar::PROPERTY_EAST_ASIAN_WIDTH);
        $width += in_array($v, $list, true) ? 2 : 1;
    }

    return $width;
}

定義した関数を試してみましょう。

$str = "あいうえお";

var_dump(
    10 === intl_east_asian_width($str),
    10 === mb_strwidth($str)
);

ベンチマーク

mb_strwidth と比べて IntlChar を使うやり方は7分の1もしくは8分の1の速度になりました。

time mb_strwidth.php
20000

real    0m0.055s
user    0m0.037s
sys 0m0.013s
time php intl_east_asian_width.php
20000

real    0m0.427s
user    0m0.408s
sys 0m0.014s
mb_strwidth.php
$str = str_repeat("あいうえおかきくけこ", 1000);

echo mb_strwidth($str), PHP_EOL;
intl_eat_asian_width.php
$str = str_repeat("あいうえおかきくけこ", 1000);

echo intl_east_asian_width($str), PHP_EOL;

function intl_east_asian_width($str, $east = true)
{
    $size = strlen($str);
    $list = [IntlChar::EA_FULLWIDTH, IntlChar::EA_WIDE];

    if ($east) {
        $list[] = IntlChar::EA_AMBIGUOUS;
    }

    $width = 0;
    $current = 0;
    $next = 0;

    while ($next < $size) {
        $c = grapheme_extract($str, 1, GRAPHEME_EXTR_MAXCHARS, $current, $next);
        $current = $next;

        $v = IntlChar::getIntPropertyValue($c, IntlChar::PROPERTY_EAST_ASIAN_WIDTH);
        $width += in_array($v, $list, true) ? 2 : 1;
    }

    return $width;
}

mb_strwidth の問題

2015年11月時点では mb_strwidth が文字の種類の判定に使うデータ (eaw_table.h) が古く、最新の EastAsianWidth.txt を反映していません。

mb_strwidth の判定テーブルと EastAsianWidth.txt の違いは次のコードで調べることができます。

$list = [IntlChar::EA_FULLWIDTH, IntlChar::EA_WIDE, IntlChar::EA_AMBIGUOUS];

for ($i = 0; $i < 0x110000; ++$i) {

    if ($i > 0xD7FF && $i < 0xE000) {
        continue;
    }

    $char = IntlChar::chr($i);

    $v = IntlChar::getIntPropertyValue($char, IntlChar::PROPERTY_EAST_ASIAN_WIDTH);
    $width = in_array($v, $list, true) ? 2 : 1;
    $width2 = mb_strwidth($char);

    if ($width !== $width2) {
        $hex = strtoupper(dechex($i));
        var_dump([$hex, $width, $width2]);
    }
}
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした