Ruby Advent Calendar 2021 - Qiita の5日目の記事です。
こんにちは。ima1zumiです。
私はRubyKaigi Takeout 2021 で Dive into Encoding というタイトルでオレオレ文字コードを作って文字コードを学ぶ話をしました。
その中で、C拡張のgemとして自作文字コードの Encoding::IROHA
をRubyで使えるようにしました。
それがこちらです。
https://github.com/ima1zumi/encoding_iroha/
このgemを使うことで、みなさんのお手元でもEncoding::IROHA
という文字コードが使えるようになります。
どう作るかとかそういった話はRubyKaigi Takeout 2021のアーカイブをご覧ください。
ですが思ったのです。C拡張のgemってビルド要るからめんどくさいですよね。
ということで、pure Ruby で文字コード変換を(雑に)実装してみました。それがこちらです。
https://github.com/ima1zumi/ruby_encoding_iroha/
今日はこのコードについて解説します。これを読み終わるころには、みなさんもRubyで文字コード変換が実装できるようになっているはずです。*1
RubyKaigi で語ったので割愛しますが、Rubyの場合はEncodingクラスの定数として使えるようにすることと変換器が必要です。
Encodingクラスの定数
Encoding
には、 replicate
というメソッドがあります。これを使うと元となった encoding と同じバイト構造を持つ Encoding クラスの定数を作ることができます。何をやっているか知りたい場合は CRuby の rb_enc_replicate
を見るとよいです。
Encoding::IROHA
は1バイト固定長の文字コードなので、同じく1バイト固定長の ASCII_8BIT
を元に IROHA
を作りましょう。
Encoding::ASCII_8BIT.replicate("IROHA")
これで Encoding::IROHA
という定数を参照できるようになります。変換器はまだ実装していないので、いろは歌への変換はできません。
replicate
の用途に興味があるので、ご存知の方がいればTwitter等で教えてもらえると嬉しいです。
変換器
Encoding::IROHA
はいろは歌+ASCIIをつかえるようにした文字コードです。RubyはCSI方式で多言語対応されているので、何に変換できるようにしてあげるか決めてあげないといけません。とりあえずUnicodeに変換できるようにしましょう。UTF-8を基本としつつ、せっかくなのでUnicode系全般に対応させてみました。
UNICODE =
[
Encoding::UTF_16,
Encoding::UTF_16BE,
Encoding::UTF_16LE,
Encoding::UTF_32,
Encoding::UTF_32BE,
Encoding::UTF_32LE,
Encoding::UTF_7,
Encoding::UTF_8,
Encoding::UTF8_DOCOMO,
Encoding::UTF8_KDDI,
Encoding::UTF8_MAC,
Encoding::UTF8_SOFTBANK,
]
るりまを見てUTFと入っているものからチョイスしたので漏れや、もはや使わなさそうな文字コードもあるかもしれませんが、大体良さそうです。
さて変換テーブルを実装しましょう。いろは歌部分は50文字もないので、変換テーブルはhashでちゃちゃっと書いてしまいまします。
UTF8_TO_IROHA_TABLE = {
"い" => "\x80",
"ろ" => "\x81",
"は" => "\x82",
"に" => "\x83",
"ほ" => "\x84",
"へ" => "\x85",
"と" => "\x86",
"ち" => "\x87",
"り" => "\x88",
"ぬ" => "\x89",
"る" => "\x8A",
"を" => "\x8B",
"わ" => "\x8C",
"か" => "\x8D",
"よ" => "\x8E",
"た" => "\x8F",
"れ" => "\x90",
"そ" => "\x91",
"つ" => "\x92",
"ね" => "\x93",
"な" => "\x94",
"ら" => "\x95",
"む" => "\x96",
"う" => "\x97",
"ゐ" => "\x98",
"の" => "\x99",
"お" => "\x9A",
"く" => "\x9B",
"や" => "\x9C",
"ま" => "\x9D",
"け" => "\x9E",
"ふ" => "\x9F",
"こ" => "\xA0",
"え" => "\xA1",
"て" => "\xA2",
"あ" => "\xA3",
"さ" => "\xA4",
"き" => "\xA5",
"ゆ" => "\xA6",
"め" => "\xA7",
"み" => "\xA8",
"し" => "\xA9",
"ゑ" => "\xAA",
"ひ" => "\xAB",
"も" => "\xAC",
"せ" => "\xAD",
"す" => "\xAE",
}
なんで\x80なのかとかはRubyKaigiのアーカイブを見てください。
変換テーブルができたので、UnicodeからIROHAへの変換を実装していきましょう。
UTF8_TO_IROHA_TABLE
でいろは歌部分とのマッチングをします。次にASCIIを変換しますが、ASCIIの場合はバイト列はUTF-8と同じなので ascii_only?
メソッドでASCIIであるかどうかだけ確認してあげます。いろは歌でもASCIIでもない文字…例えばひらがなの "が" は変換不能な文字です。ここでは Encoding::UndefinedConversionError
を上げて止めるようにしました。
def utf8_to_iroha(char)
iroha = UTF8_TO_IROHA_TABLE[char]
if iroha
iroha
else iroha.nil?
if char.ascii_only?
char
else
raise Encoding::UndefinedConversionError
end
end
end
変換不能文字の処理にバグがあってうまくいかないパターンがありますが、とりあえずこのままいきましょう。*2
文字を1文字ずつ変換して、最後に連結します。join すると encoding が IROHA ではないので、 force_encoding で Encoding::IROHA に置き換えます。
ちなみに force_encoding は、その Encoding で正しいバイト列かどうかチェックしないので注意してください。Ruby では encoding は String についているラベルのようなものなのですが、ラベルだけ張り替えて変換処理を行わないのが force_encoding です。encode はラベルの張替えだけでなく実際の変換処理を行いバイト列を書き換えます。
ここでは変換処理は utf8_to_iroha で実行しているため、最後に encoding 情報を正しくするという目的で force_encoding しています。
def to_iroha(encoding)
raise Encoding::ConverterNotFoundError unless UNICODE.include?(encoding)
self.each_char.map { |char| utf8_to_iroha(char.encode(Encoding::UTF_8)) }.join.force_encoding(Encoding::IROHA)
end
同様にIROHAからUnicodeへの変換処理を実装します。
IROHA_TO_UTF8_TABLE = UTF8_TO_IROHA_TABLE.to_h { |utf8, iroha|
[iroha.b, utf8]
}
def iroha_to_utf8(char)
utf8 = IROHA_TO_UTF8_TABLE[char.b]
if utf8
utf8
else utf8.nil?
if char.ascii_only?
char
else
raise Encoding::UndefinedConversionError
end
end
end
.b
しているのは encoding が同じでないとうまく比較処理が動かなかったためです。なんでだろう…
後はおおむね IROHA への変換と同じです。違うのは最後の encode(encoding) で、ここでは Unicode 系への変換処理を行うため Ruby 本体の変換器を使えるので、force_encoding ではなく encode しています。
def from_iroha(encoding)
raise Encoding::ConverterNotFoundError unless UNICODE.include?(encoding)
self.each_char.map { |char| iroha_to_utf8(char) }.join.encode(encoding)
end
String#encode
に IROHA 用の変換処理を挟み込みます。
from_encoding
の扱いがこれでいいのかはよくわかってないです。また、**options
には対応していません。
def encode(encoding, from_encoding = self.encoding, **options)
to_enc = convert_encoding_constant(encoding)
from_enc = convert_encoding_constant(from_encoding)
if to_enc == Encoding::IROHA
return to_iroha(from_enc)
elsif from_encoding == Encoding::IROHA
return from_iroha(to_enc)
end
super
end
これでできました。
雑にStringに追加しているのでお行儀は悪いのですが、とりあえず動くようになったのでヨシとしましょう。
感想
C拡張でしか実現出来ないと思っていたRubyへの文字コードの実装が、Rubyだけで出来て気持ちが良かったです