JRubyでUnicodeコードポイント文字列をデコードする

ハマりました。

Unicodeコードポイント文字列はあれですね。

\u3042\u3044\u3046\u3048\u304a

みたいな、一見文字化けしてるようなやつ。TwitterなんかのAPIの戻り値で見たっていう人もいるんじゃないでしょうか。そういう場合はJSONに渡してやれば勝手にデコードしてUTF-8にしてくれる(らしい)のですが、今回はJSON形式ではなく生の文字列が対象だったので除外。

普段通りCRubyのつもりで書いていたところ、いつも動くコードが全然動かない。エラーを吐くわけでもなく、デコードメソッドを作って文字列を渡しても戻り値が一向に変わる気配がない。頑なにコードポイントでいようとするのでCRubyのirbで試したところ、普通に変換できちゃいました。ここで「あぁこれJRubyの問題だな」と確信した次第です。ちなみに使っていたコードはこんな感じ。Ruby1.9です。

str.gsub(/\\u([\da-fA-F]{4})/) { [$1].pack('H*').unpack('n*').pack('U*') }

一応 jruby -E utf-8 とすれば動くっちゃ動くんですが、後のこと*1を考えるとちょっと気乗りがしませんでした。…ていうか、 -E 指定しないときは何だと思われていたのだろう。

ので、いつものようにGoogle先生にお頼み申し上げたところ、"jruby unicode" だの "jruby unicode gem" だの、検索候補が出るわ出るわ。ざっと眺めただけで結局どうしてこれがJRubyで動かないのかわからんので、なんかそれっぽい要因を勝手にリスト化して置いておきますね。

  • Java: 文字列の保持に内部ではUTF-16を使っている
  • Java: 出力するときはネイティブの文字コードに自動変換する
  • JRuby: 文字列の保持にUTF-8を使っている
  • JRuby: というよりJavaとCRubyとの互換性を保つためにUTF-8にせざるを得ない?
  • JRuby: コードにマルチバイト文字が含まれる場合、先頭におまじない*2が必要
  • JRuby: おまじないをしない場合 US-ASCII として扱われるのはCRubyと一緒
  • JRuby: そもそも # encoding: utf-8 と書けばいいのでは…?

解決しました。本当はJavaのIntegerクラスにアクセスしてparseIntメソッドを呼び出して無理やり変換していたんですが、記事を書きながら「そういえば -E オプションあったよね」と思い、試してみたら普通に動きました。過去を振り返るのも大切ですね。

というわけで良い例:

# encoding: utf-8
str.gsub(/\\u([\da-fA-F]{4})/) { [$1].pack('H*').unpack('n*').pack('U*') }

悪い例:

# -*- coding: utf-8 -*-
str.gsub(/\\u([\da-fA-F]{4})/) { [$1].pack('H*').unpack('n*').pack('U*') }  # 動かないよ><

普段Emacsさんが勝手にコメントを追加してくれるので、本来の意味を忘れてました。これファイルに含まれる文字コードRubyに伝えるだけであって、扱うエンコード(-Eオプション)を指定するわけじゃないんですよね。はーい。

すっきり解決しましたが、戒めとしてJavaのメソッドを呼び出すRuby離れしまくった暗号のようなコードも載せておきます。いい勉強になったね(白目

require 'java'
str.gsub(/\\u([\da-fA-F]{4})/) {
  java.lang.Integer.parseInt($1, 16).chr("UTF-8")
}

*1:出来るかどうか知らないけど .jar にしたりとか

*2:# -*- coding: utf-8 -*-とか