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
$str = str_repeat("あいうえおかきくけこ", 1000);
echo mb_strwidth($str), PHP_EOL;
$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]);
}
}