haxe.Utf8の非互換性

次のコードの出力を各プラットフォームで比較する。(現時点のGitHub上にある開発版を使う。)

import haxe.Utf8;

class Hello
{
    public static function main()
    {
        trace("あ𠀀い");
        trace("あ𠀀い".length);
        trace("あ𠀀い".charCodeAt(1));
        trace("あ𠀀い".charCodeAt(2));
        trace(Utf8.length("あ𠀀い"));
        trace(Utf8.charCodeAt("あ𠀀い", 1));
        trace(Utf8.charCodeAt("あ𠀀い", 2));
    }
}

文字列"あ𠀀い"はコードポイントで表すと[U+3042 U+20000 U+3044]というコードポイント数3の文字列だ。コードポイント数は文字数とは必ずしも一致しないが、コードポイント数を文字数とみなして処理をすることも多い。例えば、Twitterの140文字は、実際には140コードポイントだ。

文字列は各処理系内ではエンコーディングされた形のまま処理される。Haxeの対応しているプラットフォームでは、PHP・Neko・C++は内部エンコーディングにUTF-8を使っている。一方、JavaScriptC#Java・SWFは内部エンコーディングにUTF-16を使っている。

String#lengthは文字列のユニット数を返す。UTF-8では[e3 81 82 f0 a0 80 80 e3 81 84]、UTF-16では[3042 d840 dc00 3044]のようにエンコーディングされている。そのため "あ𠀀い".length はUTF-8の環境では10になるが、UTF-16の環境では4になる。

これでは不便なのでhaxe.Utf8が提供されているのだろうが、実際には非互換の部分がある。Utf8.lengthはUTF-8環境ではコードポイント数が返る。しかし、UTF-16環境ではそのままUTF-16のユニット数が返ってしまう。UTF-16では、BMP(U+0000〜U+FFFF)の文字だけであればユニット数とコードポイント数は一致するが、先の例のようにbeyond-BMP(U+10000〜U+10FFFF)を含む場合、この文字はサロゲートペアを使って2ユニットで表現されるので、ユニット数とコードポイント数は一致しなくなる。

haxe$ php php/index.php 
Hello.hx:7: あ𠀀い
Hello.hx:8: 10
Hello.hx:9: 129
Hello.hx:10: 130
Hello.hx:11: 3
Hello.hx:12: 131072
Hello.hx:13: 12356
haxe$ neko hello.n
Hello.hx:7: あ𠀀い
Hello.hx:8: 10
Hello.hx:9: 129
Hello.hx:10: 130
Hello.hx:11: 3
Hello.hx:12: 131072
Hello.hx:13: 12356
haxe$ cpp/Hello 
Hello.hx:7: あ𠀀い
Hello.hx:8: 10
Hello.hx:9: 129
Hello.hx:10: 130
Hello.hx:11: 3
Hello.hx:12: 131072
Hello.hx:13: 12356
haxe$ node hello.js
あ𠀀い
4
55360
56320
4
55360
56320
haxe$ mono cs/bin/cs.exe 
Hello.hx:7: あ𠀀い
Hello.hx:8: 4
Hello.hx:9: 55360
Hello.hx:10: 56320
Hello.hx:11: 4
Hello.hx:12: 55360
Hello.hx:13: 56320
haxe$ java -jar java/java.jar 
Hello.hx:7: あ𠀀い
Hello.hx:8: 4
Hello.hx:9: 131072
Hello.hx:10: 56320
Hello.hx:11: 4
Hello.hx:12: 131072
Hello.hx:13: 56320

SWFは「𠀀」の部分が豆腐に化ける以外はC#と同じ結果になる。