LoginSignup
9
8

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-11-12

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]);
    }
}
9
8
1

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
9
8