perl でカタカナ及び記号の全角半角変換をモダン?に行う

MT4i 3.1 alpha5 を公開しました(現在は alpha9 を公開しています)。昨年の YAPC にて「わわ、Encode::JP::Emoji 使わなきゃ!」と思ってはや1年。すいません鈍足で。

Encode::JP::Emoji に対応するまでには色々あって、文字化けの解消がなかなかうまく行かずに「うりゃー」っと内部文字コードを EUC-JP から UTF-8 に変更してしまいました。おかげさまで問題は解決し、まぁ、新たに些細な問題も発生しているんですけど、これでもう文字化け問題に悩まされなくても済むんじゃないかなぁ、という淡い期待を抱きつつ。

んで、これまではカタカナの全角半角変換に Encode::JP::H2Z を利用していたんですけど、これって文字コード EUC-JP が前提なんですね。せっかく EUC-JP を捨てたのに、一旦 EUC-JP に変換してまた UTF-8 に戻すという無駄なことをしないといけない訳です。

というわけで、 UTF-8 のまま全角半角変換をできないものかと思ったのですが、ググったところ、見つけたのが以下のページでした。

うむ、行き着くところは大御所。

血統書的にもこれは良いものだ、ということで迷いなく載っけたんですけど、このコードには以下のような問題点がありました。

  • 全→半変換時、ひらがなもカタカナに変換してしまう。

行いたいのはカタカナのみの変換だったので、この部分はすぐに削除しました。しかし今度は、

  • NFD はひらがなの濁音半濁音も分解し、濁音半濁音のみ半角に変換されるため、NFC で元に戻らない。
  • NFD や NFC では「神」→「神」などの変換(正規化)も行われてしまう(はてなブックマークコメントより)。

という問題も顕在化。結局、NFD や NFC の正規化を、本来とは異なる用途に使っていることで無理が出ている感じ。なので、これらを使わないようにしました。

正規化は結局、濁音半濁音から濁点半濁点を分離した上で個々に tr で変換する為に使用しているので、濁音半濁音は別個に文字コードをリストアップし、それぞれ 置換をかけるようにします。また、ここには含まれていなかった記号類の文字コードも追加して、全角半角変換するようにしました。

できたコードは以下のような感じ。


#!/usr/bin/perl
use 5.008001;
use strict;
use warnings;
use utf8;
use charnames ':full';
use Unicode::Normalize;
{
    my $hankaku = "\x{FF9E}\x{FF9F}";
    my $zenkaku = "\x{3099}\x{309A}";

    for my $o (0xFF61 .. 0xFF9D){
        $hankaku .= chr $o;
        my $n = charnames::viacode($o);
        $n =~ s/HALFWIDTH\s+//;
        $zenkaku .= chr charnames::vianame($n);
    }

    # Alphabet and sign
    $hankaku .= "\x{0021}-\x{007D}";
    $zenkaku .= "\x{FF01}-\x{FF5D}";

    # Zen dakuon/handakuon
    my @zendaku = (
    "\x{30AC}", "\x{30AE}", "\x{30B0}", "\x{30B2}",
    "\x{30B4}", "\x{30B6}", "\x{30B8}", "\x{30BA}",
    "\x{30BC}", "\x{30BE}", "\x{30C0}", "\x{30C2}",
    "\x{30C5}", "\x{30C7}", "\x{30C9}", "\x{30D0}",
    "\x{30D1}", "\x{30D3}", "\x{30D4}", "\x{30D6}",
    "\x{30D7}", "\x{30D9}", "\x{30DA}", "\x{30DC}",
    "\x{30DD}");

    # Han dakuon/handakuon
    my @handaku = (
    "\x{FF76}\x{FF9E}", "\x{FF77}\x{FF9E}",
    "\x{FF78}\x{FF9E}", "\x{FF79}\x{FF9E}",
    "\x{FF7A}\x{FF9E}", "\x{FF7B}\x{FF9E}",
    "\x{FF7C}\x{FF9E}", "\x{FF7D}\x{FF9E}",
    "\x{FF7E}\x{FF9E}", "\x{FF7F}\x{FF9E}",
    "\x{FF80}\x{FF9E}", "\x{FF81}\x{FF9E}",
    "\x{FF82}\x{FF9E}", "\x{FF83}\x{FF9E}",
    "\x{FF84}\x{FF9E}", "\x{FF8A}\x{FF9E}",
    "\x{FF8A}\x{FF9F}", "\x{FF8B}\x{FF9E}",
    "\x{FF8B}\x{FF9F}", "\x{FF8C}\x{FF9E}",
    "\x{FF8C}\x{FF9F}", "\x{FF8D}\x{FF9E}",
    "\x{FF8D}\x{FF9F}", "\x{FF8E}\x{FF9E}",
    "\x{FF8E}\x{FF9F}");

    *tr_h2z = eval "sub { local \$_ = shift; tr/$hankaku/$zenkaku/; \$_ }";
    *tr_z2h = eval "sub { local \$_ = shift; tr/$zenkaku/$hankaku/; \$_ }";

    # Zen Han Convert
    sub han2zen { my$s=shift; $s=tr_h2z($s); for(my$i=0;$i<25;$i++){$s=~s/$handaku[$i]/$zendaku[$i]/g} $s }
    sub zen2han { my$s=shift; $s=tr_z2h($s); for(my$i=0;$i<25;$i++){$s=~s/$zendaku[$i]/$handaku[$i]/g} $s }
}

binmode STDOUT, ":utf8";
local $\ = "\n";
print zen2han("「ワレワレハ、ウチュウジンダ。 アナタハ、ナニジンダ?」");
print han2zen("ウソダドンドコドーン!");

問題点あれば逐次直して行く体で。

あそうそう、Lingua::JA::Regular::Unicode という選択肢は、MT4i の性格上なしです。なるべく Core モジュール以外は使わない方向です。インストールが面倒になるので。