日本語(EUC-JP)の substr

今の Perl(5.8 以降)は文字列が utf8 だったら標準添付の substr() を使って終了なネタなわけだが、わけあって EUC-JP な文字列で日本語も 1 文字と数えて substr をするサブルーチンを考えてみた。今さら。

かの有名な Perlメモに日本語(EUC-JP)を含む文字列の split というのが(文字単位に分割する)あるんだけど、これを参考に euc_substr() というサブルーチンを書いてみた。一応 offset だけでもイケル。

#!/usr/local/bin/perl
# this script is written in EUC-JP

use strict;
use warnings;
use Benchmark;

my $ascii       = qr{ [\x00-\x7F] }xms;
my $two_bytes   = qr{ [\x8E\xA1-\xFE][\xA1-\xFE] }xms;
my $three_bytes = qr{ \x8F[\xA1-\xFE][\xA1-\xFE] }xms;

my @euc_str = (
    'となりの客は良く柿食う客だ',
    '500円1000円1万円',
    '「日本語」は英語で Japanese です',
);

timethese(
    50_000,
    {
        euc_substr => sub {
            for my $str (@euc_str) {
                my $substr = euc_substr( $str, 0, 5 );
            }
        },
        ksubstr    => sub {
            for my $str (@euc_str) {
                my $substr = ksubstr( $str, 0, 5, 'euc' );
            }
        },
    }
);

sub euc_substr {
    my $str    = shift;
    my $offset = shift;
    my $length = shift;

    my(@chars) = ( $str =~ /($ascii|$two_bytes|$three_bytes)/og );

#   if ($length) {
#       return ( join( "", @chars[ $offset .. $length ] ) );
# 恥ずかしいので直しておく
    if ( defined $length and ( $offset + $length ) <= $#chars ) {
        return ( join( "", @chars[ $offset .. ( $offset + $length - 1 ) ] ) );
    }
    else {
        return ( join( "", @chars[ $offset .. $#chars ] ) );
    }
}

sub ksubstr {
    ...
}

別に優劣を競おうってわけじゃないんだけど、ksubstr() っていうサブルーチンが perl-lib.pl ていうライブラリに収録されているので、比較ベンチマークしてみた。euc_substr() は正規表現で配列に格納してスライス、一方 ksubstr() は 一文字づつ文字を見ているようだ。

Benchmark: timing 50000 iterations of euc_substr, ksubstr...
euc_substr:  2 wallclock secs ( 2.43 usr +  0.00 sys =  2.43 CPU) @ 20578.78/s (n=50000)
   ksubstr:  7 wallclock secs ( 6.38 usr +  0.00 sys =  6.38 CPU) @ 7833.54/s (n=50000)

ksubstr は unpack( "C*", $str ); にすればもっと早いような気もするが、それはまた別の話で、使ってみた感じはどっちもそんなに重くはなさそうだ。euc_substr() は $offset と $length を全く validation していないけど、必要なら Scalar::Util::looks_like_number でも使えば良いかと思われ。

まあ、utf8 使うのが最も良い解決方法な気はするけど、EUC-JP な文字列をそのまま処理するとなるとこんな感じかなあ。もっと簡単かつ早いコードがあれば教えて欲しいです。