UTF-8文字列をバイト数でカットした時の末尾の処理

……を先日考えていたところ、NiAOUさんよりサンプルスクリプトを提示して頂きましたが、面白そうなので自分でも他のやり方がないか考えてみました(Perlクイズばりに)。

# hint: uft-8 の一文字の正規表現
#   [\x00-\x7F]|
#   [\xC0-\xDF][\x80-\xBF]|
#   [\xE0-\xEF][\x80-\xBF][\x80-\xBF]|
#   [\xF0-\xF7][\x80-\xBF][\x80-\xBF][\x80-\xBF]|
#   [\xF8-\xFB][\x80-\xBF][\x80-\xBF][\x80-\xBF][\x80-\xBF]|
#   [\xFC-\xFD][\x80-\xBF][\x80-\xBF][\x80-\xBF][\x80-\xBF][\x80-\xBF]

実のところ知りたかったのはこの部分で、最高何バイトまであり得るのかとか、各バイトは範囲がかぶったりしないのかというところが分からなかったので棚上げにしていたのでした。改めて検索してみるとManpage of UTF-8にも似たようなことが載っていたわけで、しばし黙読。なるほど、UTF-8を使ったUCS文字の符号化では最大6バイトになるけど、Unicode規格の範囲であれば最大4バイトなのか。であれば実用向けには4バイトまでとして考えてもよさそう。

Corrigendum #1: UTF-8 Shortest Formによると、2バイト文字の場合、正確には先頭バイトは[\xC2-\xDF]となるようです。

で、UTF-8の正規表現をじっくり見つつ考えたところ、完全な1文字にならないバイト列が余った時について場合分けしてみればいいかと思いつきまして、

my $utf8 = "「UTF-8文字列末尾の余りbyteをcut」";

for (my $i = 0; $i <= 42; $i++) {
    my $a = substr($utf8, 0, length($utf8) - $i);
    print round_utf8($a), "\n";
}

sub round_utf8 {
    my $str = shift;
    return $str if ($str =~ /[\x00-\x7F]$/);
    $str =~ s/[\xC0-\xFD]$//;                #1バイト余った場合
    $str =~ s/[\xE0-\xFD][\x80-\xBF]$//;     #2バイト余った場合
    $str =~ s/[\xF0-\xFD][\x80-\xBF]{2}$//;  #3バイト余った場合
#   $str =~ s/[\xF8-\xFD][\x80-\xBF]{3}$//;  #4バイト余った場合
#   $str =~ s/[\xFC-\xFD][\x80-\xBF]{4}$//;  #5バイト余った場合
    $str;
}

……としてみましたがどうでしょう。

(2003年10月23日)

北村曉 kits@akatsukinishisu.net