String#force_encodingは文字コードの強制変換ではない

RubyString#force_encoding は String の encoding を変更するだけで、文字コードの変換を行うわけではありません。そのため、バイト列は変換されません。

force_encoding はどんなメソッドか

String の encoding を変更します。ただし、バイト列は変更せず、encoding に対しバイト列が正しいかどうかは確認しません。

例えば EUC-JP に変換して encode と比較します。

''.encode(Encoding::EUC_JP).encoding
# => #<Encoding:EUC-JP>

''.force_encoding(Encoding::EUC_JP).encoding
# => #<Encoding:EUC-JP>

どちらも encoding は EUC-JP です。

String#valid_encoding? で正当なバイト列かどうか確認できます。

''.encode(Encoding::EUC_JP).valid_encoding?
# => true

''.force_encoding(Encoding::EUC_JP).valid_encoding?
# => false

force_encodingfalse になり、バイト列が正しくないことがわかります。

encodeforce_encoding の返り値を見てみると、

''.encode(Encoding::EUC_JP)
# => "\x{A4A2}"

''.force_encoding(Encoding::EUC_JP)
# => "\xE3\x81\x82"

encodeforce_encoding で結果が異なっています。

どちらも のバイト列ですが、encode の返り値の \xA4\xA2EUC-JP のバイト列で、 force_encoding のほうの \xE3\x81\x82UTF-8 のバイト列です。

''.encode.bytes.map { _1.to_s(16) }
# => ["e3", "81", "82"]

encode は encoding とバイト列を変更しますが、 force_encoding は encoding のみ変更してバイト列は変更しません。

force_encoding はどんなときに使うか

バイト列を変えずに encoding だけを変換したいときに使います。

Array#pack の返り値に対して encoding を設定する場合

Array#pack の返り値の encoding は ASCII-8BIT だったりして人間には読みにくく、また encoding に UTF-8 を期待している場合に使いにくいので、UTF-8 (もしくはお使いの環境の文字コード)の文字列として扱いたい場合 force_encoding をつけると使いやすくなります。

["e38182"].pack("H*")
# => "\xE3\x81\x82"

["e38182"].pack("H*").force_encoding(Encoding::UTF_8)
# => "あ"

バイト列から文字を組み立てる場合

バイト列だけが分かっていて、encoding を後から設定したい場合です。

例えば以下の記事に書いたような、未定義文字に対する変換先の文字を定義する場合です。これを encode で同じことをすると少しややこしくなるので force_encoding したほうがすっきりします。

U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError) に対応する - esm アジャイル事業部 開発者ブログ

str = "\u{2014 301C 2016 2212 00A2 00A3 00AC}"

undefined_signs = {
  "\u2014" => "\x81\x5C".force_encoding(Encoding::Windows_31J), # — EM DASH
  "\u301C" => "\x81\x60".force_encoding(Encoding::Windows_31J), # 〜 WAVE DASH
  "\u2016" => "\x81\x61".force_encoding(Encoding::Windows_31J), # ‖ DOUBLE VERTICAL LINE
  "\u2212" => "\x81\x7C".force_encoding(Encoding::Windows_31J), # − MINUS SIGN
  "\u00A2" => "\x81\x91".force_encoding(Encoding::Windows_31J), # ¢ CENT SIGN
  "\u00A3" => "\x81\x92".force_encoding(Encoding::Windows_31J), # £ POUND SIGN
  "\u00AC" => "\x81\xCA".force_encoding(Encoding::Windows_31J), # ¬ NOT SIGN
}

p str.encode(Encoding::Windows_31J, fallback: undefined_signs)

この場合は String.new でも同じことができます。

String.new("\x81\xCA", encoding: Encoding::CP932)

DB に入れるデータをバイナリ扱いしたい場合

普通にやると別の文字コードに変換されてしまう文字を DB の設定を変えずに DB に格納したいことがあり、そのために encoding に Encoding::ASCII_8BIT をつけてバイナリ扱いして encoding を変換させずに入れるために使ったことがあります。

まとめ

force_encoding では encoding のみを変更し、バイト列は変更しません。また、バイト列が正しいかどうかのチェックも行いません。

encoding に対してバイト列が不正な場合は文字を正しく扱えないため、どうしても force_encoding したい場合は valid_encoding? とセットで正しいバイト列かチェックして使う方が安全だと思います。

参考

Ruby文字コード対応については以下の記事がとても詳しいので、興味のある方はぜひこちらを参照ください。

Ruby M17N の設計と実装