続・U+3099, U+309A

おとといの日記で書いてた PHP5 用の濁点・半濁点補正クラス。スクリプト文字コードUTF-8
清音の第三バイトに 1 (半濁音では 2) 加算する方法も考えられるけど、このぐらいの文字数なら対応表を作ってもほとんどメモリを消費しないので、連想配列の対応表を使うことにした。

final class AppleHFS
{
    const re = '/[かきくけこさしすせそたちつてとはひふへほカキクケコサシスセソタチツテトハヒフヘホ]\\x{3099}|[はひふへほハヒフヘホ]\\x{309A}/u';

    private static $map = array(
        "か\xE3\x82\x99" => 'が', "き\xE3\x82\x99" => 'ぎ', "く\xE3\x82\x99" => 'ぐ', "け\xE3\x82\x99" => 'げ', "こ\xE3\x82\x99" => 'ご',
        "さ\xE3\x82\x99" => 'ざ', "し\xE3\x82\x99" => 'じ', "す\xE3\x82\x99" => 'ず', "せ\xE3\x82\x99" => 'ぜ', "そ\xE3\x82\x99" => 'ぞ',
        "た\xE3\x82\x99" => 'だ', "ち\xE3\x82\x99" => 'ぢ', "つ\xE3\x82\x99" => 'づ', "て\xE3\x82\x99" => 'で', "と\xE3\x82\x99" => 'ど',
        "は\xE3\x82\x99" => 'ば', "ひ\xE3\x82\x99" => 'び', "ふ\xE3\x82\x99" => 'ぶ', "へ\xE3\x82\x99" => 'べ', "ほ\xE3\x82\x99" => 'ぼ',
        "は\xE3\x82\x9A" => 'ぱ', "ひ\xE3\x82\x9A" => 'ぴ', "ふ\xE3\x82\x9A" => 'ぷ', "へ\xE3\x82\x9A" => 'ぺ', "ほ\xE3\x82\x9A" => 'ぽ',
        "カ\xE3\x82\x99" => 'ガ', "キ\xE3\x82\x99" => 'ギ', "ク\xE3\x82\x99" => 'グ', "ケ\xE3\x82\x99" => 'ゲ', "コ\xE3\x82\x99" => 'ゴ',
        "サ\xE3\x82\x99" => 'ザ', "シ\xE3\x82\x99" => 'ジ', "ス\xE3\x82\x99" => 'ズ', "セ\xE3\x82\x99" => 'ゼ', "ソ\xE3\x82\x99" => 'ゾ',
        "タ\xE3\x82\x99" => 'ダ', "チ\xE3\x82\x99" => 'ヂ', "ツ\xE3\x82\x99" => 'ヅ', "テ\xE3\x82\x99" => 'デ', "ト\xE3\x82\x99" => 'ド',
        "ハ\xE3\x82\x99" => 'バ', "ヒ\xE3\x82\x99" => 'ビ', "フ\xE3\x82\x99" => 'ブ', "ヘ\xE3\x82\x99" => 'ベ', "ホ\xE3\x82\x99" => 'ボ',
        "ハ\xE3\x82\x9A" => 'パ', "ヒ\xE3\x82\x9A" => 'ピ', "フ\xE3\x82\x9A" => 'プ', "ヘ\xE3\x82\x9A" => 'ぺ', "ホ\xE3\x82\x9A" => 'ポ',
    );

    public static function normalize($path)
    {
        return preg_replace_callback(self::re, array(self, '_normalize'), $path);
    }

    private static function _normalize($m)
    {
        return self::$map[$m[0]];
    }
}

せっかくなので PHP4 でも使える関数として作り直してみた。

function normalizeAppleHFS($path)
{
    $map = array(
        (略)
    );
    return preg_replace('/[かきくけこさしすせそたちつてとはひふへほカキクケコサシスセソタチツテトハヒフヘホ]\\x{3099}|[はひふへほハヒフヘホ]\\x{309A}/ue', '$map["$0"]', $path);
}


テストコード:

$s0 = "ハ\xE3\x82\x9Aク\xE3\x82\x99";
printf("unicode string(%d) %s\n", mb_strlen($s0), $s0);

$s1 = AppleHFS::normalize($s0);
printf("unicode string(%d) %s\n", mb_strlen($s1), $s1);

$s2 = normalizeAppleHFS($s0);
printf("unicode string(%d) %s\n", mb_strlen($s2), $s2);

結果:

unicode string(4) パグ
unicode string(2) パグ
unicode string(2) パグ


実際のプログラムではこんな感じ。*1
フォームの accept-charset 属性を UTF-8 にし、php.ini で自動エンコーディング変換を切っておくのも忘れずに。

if ($_FILES['userfile']['error'] == UPLOAD_ERR_OK) {
    $filename = get_magic_quotes_gpc() ? stripslashes($_FILES['userfile']['name']) : $_FILES['userfile']['name'];
    $filename = AppleHFS::normalize($filename);
    $timestamp = time();
    move_uploaded_file($_FILES['userfile']['tmp_name'], "data/$timestamp");
    $sql = sprintf("INSERT INTO uploaded_file VALUES(%d, %s);", $timestamp, $db->quote($filename));
    $result = $db->query($sqll);
    if (DB::isError($result)) {
        throw new Exception($result->getMessage(), $result->getCode());
    }
}


ちなみにテストコードでやった変換を100,000回繰り返すベンチマークをしてみると、クラス版の方が関数版より3倍ほど速かった。
$map への代入が毎回行われるのと、eval を使っているのが原因。*2
ただしファイルのアップロードで使われることを考えると、その差はこれを利用するプロセス全体からすれば誤差の範囲に収まる場合がほとんどだと思うので、関数版でも特に問題ないはず。

*1:サンプルコードといえど、最低限のサニタイズとエラー処理は意識しておく

*2:$map をグローバル変数にし、コールバック関数を使うと関数版の方が2割ほど速くなった