いまさら聞けない!波ダッシュと全角チルダ問題についてまとめてみた
元々のきっかけは以下のように Ruby で UTF-8
の波文字を SJIS
に変換しようとしたらエラーになってしまいました。
# UTF-8 の 〜 文字を SJIS に変換するとエラーになる # error: `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError) "〜".encode("SJIS")
これの原因を調べてみたら 波ダッシュと全角チルダ問題 にたどり着いたのでその問題と歴史についてまとめてみました。
諸注意
波ダッシュと全角チルダ問題 に関しては一般的な話なんですが、変換ルールやエンコーディングの指定の仕方などは CRuby での話になります。
他の言語や処理系だと結果が変わるかもしれないので注意してください。
また、この記事でのソースコードはすべて UTF-8
になります。
それでは本題に入っていきましょう。
Ruby で UTF-8
の波文字(〜
)を SJIS
に変換するとエラーになる
冒頭にも記述したように元々この問題を調べようとしたきっかけは次のように Ruby で UTF-8
の波文字(〜
)を SJIS
に変換しようとするとエラーになってしまうことでした。
# UTF-8 の 〜 文字を SJIS に変換するとエラーになる # error: `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError) "〜".encode("SJIS")
上記のコードで変換しようとした波文字(〜
)は Unicode
のコードポイントだと U+301C
になるんですが、その文字が SJIS
への変換に失敗 しています(ちなみに Ruby では SJIS
が Windows-31J
のエイリアスとして定義されています)。
# Ruby で Unicode のコードポイントを表示する pp "〜".unpack1("U*").to_s(16) # => "301c"
SJIS
にも波文字自体は存在しているのに不思議ですよね?
そもそも波文字とはどういう文字なのでしょうか。
Unicode
の波文字について詳しくみてみましょう。
波文字は2つあった!
実は Unicode
には波文字を表す文字が2つ存在しています。
それが『波ダッシュ文字(U+301C
)』と『全角チルダ文字(U+FF5E
)』になります。
最初に Ruby のコードで記述していた波文字は 波ダッシュ文字(U+301C
) になります。
文字 | 意味 | Unicode のコードポイント | UTF-8 のバイト列 |
---|---|---|---|
〜 |
波ダッシュ(WAVE DASH) | U+301C |
\xE3809C |
~ |
全角チルダ(FULLWIDTH TILDE) | U+FF5E |
\xEFBD9E |
この2つの文字は見た目上は似た文字として表示されていると思うのですが、実際には全く異なる文字になります。 試しにここに書いてある文字をバイナリエディタにコピペしてみたり Ruby でコードポイントを取得してみると全然違う文字だと言うことがわかると思います。
# Unicode のコードポイントを出力してみる # これは波ダッシュ pp "〜".unpack1("U*").to_s(16) # => "301c" # これは全角チルダ pp "~".unpack1("U*").to_s(16) # => "ff5e"
このように 〜
が波ダッシュ(U+301C
) と ~
が全角チルダ(U+FF5E
) は全く異なる文字になります。
この『Unicode
には波文字が2種類存在する事』が SJIS
に変換できない問題と深く関わってきます。
全角チルダ ~
(U+FF5E
) は SJIS
に変換できる
先程紹介した Unicode
の2つの波文字なのですが 波ダッシュ 〜
(U+301C
) は SJIS
に変換するとエラーになることはわかりました。
# これは波ダッシュ nami = "〜" pp nami.unpack1("U*").to_s(16) # => "ff5e" # SJIS に変換するとエラーになる pp nami.encode("SJIS") # => "\x{8160}"
しかし、実は 全角チルダ ~
(U+FF5E
) は SJIS
に変換する事ができます。
# これは全角チルダ tilde = "~" pp tilde.unpack1("U*").to_s(16) # => "ff5e" # SJIS に変換する事ができる! pp tilde.encode("SJIS") # => "\x{8160}"
これは一体どういうことなのでしょうか。
波ダッシュ 〜
(U+301C
) を Shift_JIS
に変換すると…
ここでは変換後のエンコーディングの話をします。
Ruby では SJIS
と名前が似ている Shift_JIS
というエンコーディングも存在しています。
実は SJIS
ではなくて Shift_JIS
であれば UTF-8
の波ダッシュ 〜
(U+301C
) を Shift_JIS
に変換することができます。
# これは波ダッシュ nami = "〜" pp nami.unpack1("U*").to_s(16) # => "301c" # Shift_JIS に変換する事ができる! pp nami.encode("Shift_JIS") # => "\x{8160}"
また逆に UTF-8
の 全角チルダ ~
(U+FF5E
) を Shift_JIS
に変換すると エラー になります。
# これは全角チルダ tilde = "~" pp tilde.unpack1("U*").to_s(16) # => "ff5e" # SJIS に変換するとエラーになる # error: `encode': U+FF5E from UTF-8 to Shift_JIS (Encoding::UndefinedConversionError) pp tilde.encode("Shift_JIS")
Ruby の変換ルールとしては以下のようになっています。
変換前の UTF-8 の文字 |
SJIS |
Shift_JIS |
---|---|---|
波ダッシュ 〜 (U+301C ) |
変換できない | 変換できる |
全角チルダ ~ (U+FF5E ) |
変換できる | 変換できない |
次は SJIS
と Shift_JIS
の違いについて調べてみましょう。
Ruby における SJIS
と Shift_JIS
の違い
ここでは Ruby における SJIS
と Shift_JIS
の違いについて簡単に解説します。
まず Shift_JIS
はその名の通り文字コードの Shift_JIS
のことを指しています。
Shift_JIS
とは日本語を含む文字列を表現するために用いられる文字コードになります。
また Shift_JIS
は JIS X 0208
として標準化されています。
Ruby ではこの文字コードが Shift_JIS
として利用できます。
次に SJIS
なのですが Ruby のエンコーディングにおいて SJIS
は CP932
Windows-31J
と同じエンコーディングとして定義されています。
Windows-31J
とは Microsoft が Shift_JIS
を独自拡張したエンコーディングになります。
例えば ①
という文字は Shift_JIS
には存在しませんが Windows-31J
には存在する文字になります。
# Windows-31J に変換可能 pp "①".encode("Windows-31J") # => "\x{8740}" # SJIS や CP932 も同様 pp "①".encode("SJIS") # => "\x{8740}" pp "①".encode("CP932") # => "\x{8740}" # Shift_JIS には変換できない # error: U+2460 from UTF-8 to Shift_JIS (Encoding::UndefinedConversionError) pp "①".encode("Shift_JIS")
なので厳密に言うと SJIS
と Shift_JIS
は 異なるエンコーディング になります。
まとめると Ruby において SJIS
と Shift_JIS
は以下のような関係になっています。
SJIS
とCP932
Windows-31J
の 3つは同じエンコーディングとして扱われているWindows-31J
(SJIS
CP932
) は Microsoft がShift_JIS
を独自拡張したエンコーディングになるShift_JIS
とWindows-31J
は異なるエンコーディングになるShift_JIS
もWindows-31J
もJIS X 0208
という規格に準じている
より詳しい違いが知りたい人は以下を参照してください。
- Shift_JIS と Windows-31J (MS932) の違いを整理してみよう |
- Python♪Windowsの「Shift JIS」の落とし穴 | Snow Tree in June
- Encoding::SHIFT_JIS (Ruby 3.1 リファレンスマニュアル)
- Encoding::CP932 (Ruby 3.1 リファレンスマニュアル)
以降は SJIS
という表記だとややこしいので CP932
と表記し『 CP932
と Shift_JIS
』について解説していきます。
JIS X 0208
の波文字とは
ここでは Shift_JIS
や CP932
の規格である JIS X 0208
の話をします。
実は JIS X 0208
で定義されている波文字は Unicode
とは違って『波ダッシュ文字(\x8160
)だけ』が存在しています。
(JIS X 0208
を更に拡張した JIS X 0213
には全角チルダも定義されているんですがここでは一旦置いておきます。
各エンコーディングの波文字の情報は以下のようになります。
文字 | Unicode のコードポイント | UTF-8 のバイト列 |
JIS X 0208 のバイト列 |
---|---|---|---|
波ダッシュ(WAVE DASH) 〜 |
U+301C |
\xE3809C |
\x8160 |
全角チルダ(FULLWIDTH TILDE) ~ |
U+FF5E |
\xEFBD9E |
ない |
なので UTF-8
の波ダッシュを Shift_JIS
に変換すると Shift_JIS
の波ダッシュに変換されるのは期待する挙動と言えます。
# 波ダッシュを Shift_JIS に変換すると Shift_JIS の波ダッシュになる p "\u301C".encode("Shift_JIS") # => "\x{8160}"
では、なぜ UTF-8
の波文字を CP932
に変換するときに『対応する文字がある波ダッシュがエラー』になって『対応する文字がない全角チルダがエラー』になるのでしょうか。
# 全角チルダを CP932 に変換するとなぜか JIS X 0208 の波ダッシュになる p "\uFF5E".encode("CP932") # => "\x{8160}" # 波ダッシュを CP932 に変換するとJIS X 0208 の波ダッシュは存在するのにエラーになる # error: `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError) p "\u301C".encode("CP932") # => "\x{8160}"
この疑問を解消するためにまず JIS X 0208
の波ダッシュ、つまり『 CP932
や Shift_JIS
から UTF-8
に変換するとどうなるのか』を試してみましょう。
JIS X 0208
の波ダッシュを UTF-8
に変換するとどうなる?
今度は逆に JIS X 0208
の波ダッシュを UTF-8
に変換するとどうなるのか見てみましょう。
実際に Ruby で Shift_JIS
や CP932
から波ダッシュを UTF-8
に変換してみます。
Ruby で変換する場合は JIS X 0208
の波ダッシュ文字をバイト列で定義し String#encode
に変換元となるエンコーディングの情報として CP932
と Shift_JIS
をそれぞれ指定して UTF_8
に変換してみます。
# CP932 でも Shift_JIS でもバイト列は同じ jis_nami = "\x81\x60" # CP932 -> UTF_8 に変換すると全角チルダになる pp jis_nami.encode("UTF-8", "CP932").unpack1("U*").to_s(16) # => "ff5e" # Shift_JIS -> UTF_8 に変換すると波ダッシュになる pp jis_nami.encode("UTF-8", "Shift_JIS").unpack1("U*").to_s(16) # => "301c"
このように Shift_JIS -> UTF_8
は『同じ文字への変換』になっています。
しかし CP932 -> UTF_8
は『異なる文字への変換』になっていることがわかります。
文字コード | 変換前 | UTF-8 へ変換 |
---|---|---|
CP932 |
波ダッシュ | 全角チルダ |
Shift_JIS |
波ダッシュ | 波ダッシュ |
この『 CP932
の波ダッシュを UTF-8
に変換したときに誤った文字になってしまうこと』がいわゆる 波ダッシュ問題 として一般的に扱われている問題になります。
では、なぜこのような変換になってしまったのでしょうか。
Unicode
の歴史を調べてみましょう。
Unicode
の波ダッシュの例示字形が間違っていた
結論からいうと Unicode 7.0
で定義されていた波ダッシュの 例示字形 が間違った形で記載されてしまっていたのが要因になっているようです。
どういうことかと言うと現在の Unicode
では波ダッシュも全角チルダも ~
のように 上がって下がる ような字形になります。
これは JIS X 0208
も同様の字形になっています。
しかし Unicode 7.0
では波ダッシュが以下のように 下がって上がる という文字として例示字形が記載されていました。
文字コード | 波ダッシュ | 全角チルダ |
---|---|---|
Unicode 7.0 | ![]() ※これが間違い |
![]() |
Unicode 8.0 | ![]() |
![]() |
JIS X 0208 | ![]() |
実際に Unicode
のコードチャートにも以下のように記載されています。
Unicode 7.0
Unicode 8.0
WAVE DASH
の例示字形が異なる形になっているのがわかると思います。
これが CP932 -> UTF-8
に変換した際に 波ダッシュ -> 全角チルダ
となってしまっている要因になっています。
この Unicode 7.0
と Unicode 8.0
の波ダッシュの例示字形の問題に関しては以下の記事で詳しく解説されているので気になる人は読んでみるとよいと思います。
- UnicodeのWAVE DASH例示字形が、25年ぶりに修正された理由 - INTERNET Watch Watch
- ~波ダッシュ~って字形が変わった?|IT情報メディアサイト idearu(アイディアル)
問題は逆だった
ここからは仮説になってくるのですが先程説明したように JIS X 0208
の波ダッシュと Unicode 7.0
の波ダッシュでは字形が異なっていました。
なのでそのまま JIS X 0208
の波ダッシュを UTF-8
の波ダッシュに変換してしまうと 文字としては同じ なのですが 見た目としては違う文字 に見えてしまいます。
この問題を回避するために Windows では JIS X 0208
の波ダッシュを UTF-8
に変換する時に『 JIS X 0208
の波ダッシュと同じ字形であった全角チルダに変換する』というルールができたのではないかと考えられます。
これにより Windows
で使われている CP932
は『波ダッシュ -> 全角チルダ
』という変換になり、逆に JIS X 0208
と同等である Shift_JIS
はそのままの文字である『波ダッシュ -> 波ダッシュ
』になっているのではなかろうかと思います。
文字コード | 変換前 | UTF-8 へ変換 |
---|---|---|
CP932 |
波ダッシュ | 全角チルダ |
Shift_JIS |
波ダッシュ | 波ダッシュ |
逆に UTF-8 -> CP932
と変換するときも同様に『 全角チルダ -> 波ダッシュ
』となり UTF-8 -> Shift_JIS
だと『 波ダッシュ -> 波ダッシュ
』になっているのだと予想しています。
文字コード | 変換前 | CP932 へ変換 |
Shift_JIS へ変換 |
---|---|---|---|
UTF-8 |
波ダッシュ | エラー | 波ダッシュ |
UTF-8 |
全角チルダ | 波ダッシュ | エラー |
まとめ
Unicode
の波文字は 波ダッシュ文字(U+301C
) と 全角チルダ文字(U+FF5E
) の2種類ある
文字 | 意味 | Unicode のコードポイント | UTF-8 のバイト列 |
---|---|---|---|
〜 |
波ダッシュ(WAVE DASH) | U+301C |
\xE3809C |
~ |
全角チルダ(FULLWIDTH TILDE) | U+FF5E |
\xEFBD9E |
- Ruby では
SJIS
とShift_JIS
は異なるエンコーディングとして定義されているSJIS
はCP932
とWindows-31J
と同等のエンコーディングとして定義されている
- Ruby では以下のようなルールで変換される
変換前の文字コード | 変換後の文字コード | 変換前の文字 | 変換後の文字 |
---|---|---|---|
UTF-8 |
CP932 |
波ダッシュ(U+301C ) |
エラー |
UTF-8 |
CP932 |
全角チルダ(U+FF5E ) |
波ダッシュ(\x8160 ) |
UTF-8 |
Shift_JIS |
波ダッシュ(U+301C ) |
波ダッシュ(\x8160 ) |
UTF-8 |
Shift_JIS |
全角チルダ(U+FF5E ) |
エラー |
CP932 |
UTF-8 |
波ダッシュ(\x8160 ) |
全角チルダ(U+FF5E ) |
Shift_JIS |
UTF-8 |
波ダッシュ(\x8160 ) |
波ダッシュ(U+301C ) |
参照
余談:Ruby で Unicode の波ダッシュを SJIS の波ダッシュに変換する
以下のように #encode
の fallback
オプションで制御する事が可能です。
pp "〜".encode("SJIS", fallback: { "\u301C" => "\x81\x60".force_encoding("SJIS") })
別解としては #tr
で波ダッシュを全角チルダに変換してから #encode
するやり方も考えられます。
pp "〜".tr("\u301C", "\uFF5E").encode("SJIS")
余談: iconv
の変換ルール
iconv
だと次のような変換ルールとなっています。
変換前の文字コード | 変換後の文字コード | 変換前の文字 | 変換後の文字 |
---|---|---|---|
UTF-8 |
CP932 |
波ダッシュ(U+301C ) |
波ダッシュ(U+301C ) ※これが Ruby と異なる |
UTF-8 |
CP932 |
全角チルダ(U+FF5E ) |
波ダッシュ(\x8160 ) |
UTF-8 |
Shift_JIS |
波ダッシュ(U+301C ) |
波ダッシュ(\x8160 ) |
UTF-8 |
Shift_JIS |
全角チルダ(U+FF5E ) |
エラー |
CP932 |
UTF-8 |
波ダッシュ(\x8160 ) |
全角チルダ(\xEFBD9E ) |
Shift_JIS |
UTF-8 |
波ダッシュ(\x8160 ) |
波ダッシュ(\xE3809C ) |
以下、実行ログ。
# utf-8 の波ダッシュ -> cp932 は変換できる $ echo -n "〜" | iconv -f utf-8 -t cp932 | od -tx1 -An 81 60 # utf-8 の全角チルダ -> cp932 は変換できる $ echo -n "~" | iconv -f utf-8 -t cp932 | od -tx1 -An 81 60
# utf-8 の波ダッシュ -> shift_jis は変換できる $ echo -n "〜" | iconv -f utf-8 -t shift_jis | od -tx1 -An 81 60 # utf-8 の全角チルダ -> shift_jis は変換できない $ echo -n "~" | iconv -f utf-8 -t shift_jis | od -tx1 -An iconv: 位置 0 に不正な入力シーケンスがあります
# cp932 -> utf-8 は全角チルダ $ echo -n $'\x81\x60' | iconv -f cp932 -t utf-8 | od -tx1 -An ef bd 9e # shift_jis -> utf-8 は波ダッシュ $ echo -n $'\x81\x60' | iconv -f shift_jis -t utf-8 | od -tx1 -An e3 80 9c
UTF-8 -> CP932
の変換だけ Ruby と異なっています。