Ruby で UTF-8 の文字列を SJIS で文字化けさせたり復元したりする

以下の記事を知人と話していたら思ったよりも盛り上がったので覚書。

ちなみにクイズのネタバレがあるので見たくない人は読まないでね!!

縺ゅ¢縺セ縺励※縺翫a縺ァ縺ィ縺 みたいな文字化けした文字列を Ruby で生成する

言及されている文字化けは『 UTF-8 な文字を SJIS で表示すると文字化ける』っていうような話になります。
では、Ruby でこのような文字化けを行う場合どうするのがいいのでしょうか。
これは要するに『 UTF-8SJIS として扱い UTF-8 で変換する』という処理で実現することができます。
まず、Ruby のデフォルトのエンコーディングutf-8 なので単に文字列リテラルを定義した場合は utf-8 の文字列になります。

# 文字列リテラルは utf-8
utf8 = "やばたにえん"
pp utf8.encoding
# => #<Encoding:UTF-8>

次に String#force_encoding を使って『内部のデータはそのままでエンコーディング情報のみ』を変更します。
NOTE: ちなみに #force_encoding#encode と違い #valid_encoding でチェックは行いません(thanks id:imaizumimr :)

utf8 = "やばたにえん"

# 内部のエンコーディング情報のみ書き換える
# 内部データはそのまま
sjis = utf8.force_encoding(Encoding::SJIS)

# エンコーディング情報のみ SJIS として扱われる
# ちなみに SJIS は Windows-31J のエイリアス
pp sjis.encoding
# => #<Encoding:Windows-31J>

# バイトコードは同じ
pp utf8.bytes == sjis.bytes
# => true

最後に『内部データは UTF-8 だけど文字コード情報 SJIS 』な文字列に対して UTF-8エンコードします。
この時に変換できない文字があるので無理やり変換されるように invalid: :replace, undef: :replace オプションを指定しています。

utf8 = "やばたにえん"

sjis = utf8.force_encoding(Encoding::SJIS)

# SJIS だけど実データは UTF-8 な文字列を無理やり UTF-8 で変換させる
pp sjis.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
# => "繧��縺溘↓縺医s"

これでいわゆる『文字化け』した文字列が生成できました。

文字化けしたコードを戻す

では、次は文字化けしたコードを復元してみましょう。
現状はこんな感じです。

utf8 = "やばたにえん"

sjis = utf8.force_encoding(Encoding::SJIS)
bake = sjis.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)

pp bake
# => "繧��縺溘↓縺医s"
pp bake.encoding
# => #<Encoding:UTF-8>

これを元に戻すのは比較的簡単で(全然簡単ではなかったけど…)先程の処理と逆のことをします。

utf8 = "やばたにえん"

sjis = utf8.force_encoding(Encoding::SJIS)
bake = sjis.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)

# まず SJIS でエンコードする。その後に文字コードを UTF-8 に指定する
pp bake.encode(Encoding::SJIS, invalid: :replace, undef: :replace).force_encoding(Encoding::UTF_8)
# => "\xE3\x82??たにえん"

これで復元することができます。
できるんですが文字化けさせるときのエンコードを無理やり行っているので上の手順で復元した場合は完璧に復元することはできません、うぐぅ…。

まとめ

文字コードなんもわからんが RubyString文字コード周りの実装がしっかりしててすごいなぁ。
普段書かないような処理なのでいろいろと知らない機能やオプションなんかの知見がしれてよかったです。
あと人とわいわいしながらコード書くのたのしいですね!!!
久々に書いてて楽しい Ruby のコードだった。