VB.NETでも独自のエンコーディングを実装したい(前編)

さぁエディタが下からグイッとパンしてビルドエラーがどーん

窓際で軽く・・・じゃなくてどうもこんにちは。

最近は名前空間の別名だ等値演算子だのよくある『VB.NETお助けまとめサイト』の内容の様なフツーにmsdnを読めば分かるようなちょっとしたテクニック紹介みたいになっていましたが、もともとこのブログのコンセプトは『使うメリットはまるでないむしろ害悪にしかならないゴミテクニックの紹介もしくは標準ライブラリの劣化再実装』と決めていたので[要出典]、何か物足りなさを感じていたのです。

何が足りないか。そう、文字コード成分が足りない

少し前にPythonで独自の文字コードを実装しましたよね。 あれを今回はVB.NETでやっちゃうというという誰も望まないし誰も得しない企画です。

符号化文字集合及び文字符号化方式Python版から流用します。

ドキュメントは大切に

System.Text 名前空間 ()

とりあえず.NETでの文字コードはここから始めればいいのかな。 .NETの文字コード関係はSystem.Text名前空間に集められているようです。

パッと見た感じでは

  1. EncoderDecoderの実装
  2. EncoderDecoderを用いてEncodingを実装

といった流れです。

また、ユーザはエンコーダ・デコーダに対してフォールバック戦略を指定することができます。 逆に言うとエンコードの開発者はユーザに対してフォールバック戦略を選択できる手段を提供しないといけないってことです。 これはPythonではerrorsオプションで指定していたエラー処理スキームにあたります。

.NET Framework における文字エンコーディング

フォールバック戦略はエンコード時もしくはデコード時に変換できない文字・バイト列を検出した時にエンコーダ・デコーダが取りうる挙動を規定するものです。

  • 最適フォールバック
  • 置換フォールバック
    • ターゲットエンコードに厳密な一致がなくマップできる適切な文字も無い場合は諦めて適当な文字で置き換える戦略です。
  • 例外フォールバック
    • エンコードできない文字を見つけた場合やデコードできないバイト列を検出した場合は例外をスローして呼び出し元に通知します。
    • Pythonでやった時はコレのみのサポートとしました。

最適な戦略はターゲットエンコードによって変わるので明確に文書化はしないそうです。

エンコーダ・デコーダの実装やエンコーディングの実装もサンプルコードや詳しい説明が無いんですよね。 もともとユーザが拡張するなんてよほどのことがない限りやりませんからね。 自作したエンコーディングEncoding.GetEncoding()からエンコード名で呼び出す方法も公開されてないっぽいです。

とりあえず今回はフォールバックについて一通り確認して終わりたいです。

フォールバック

まぁ上ではフォールバックストラテジとか書いちゃってますけど、つまるところエンコーダ・デコーダが正常に処理を続行出来ない時にどうさせるかって問題です。

Imports System
Imports System.Text

Module Module1

    Sub Main()
        FallbackTest(
            Encoding.GetEncoding("us-ascii"), "hello Å こんにちは")

        FallbackTest(
            Encoding.GetEncoding(
            "us-ascii",
            New EncoderExceptionFallback(),
            New DecoderExceptionFallback()
            ), "hello Å こんにちは")

        FallbackTest(Encoding.GetEncoding(
            "us-ascii",
            New EncoderReplacementFallback("*"),
            New DecoderReplacementFallback("*")
            ), "hello Å こんにちは")

        FallbackTest(Encoding.GetEncoding(
            "Shift_JIS"), "hello Å こんにちは")

        FallbackTest(
            Encoding.GetEncoding(
            "Shift_JIS",
            New EncoderExceptionFallback(),
            New DecoderExceptionFallback()
            ), "hello Å こんにちは")

        FallbackTest(Encoding.GetEncoding(
            "Shift_JIS",
            New EncoderReplacementFallback("*"),
            New DecoderReplacementFallback("*")
            ), "hello Å こんにちは")
    End Sub

    Sub FallbackTest(encoding As Encoding, target As String)
        Console.WriteLine("Encoding name : {0}", encoding.EncodingName)
        Console.WriteLine(
            "Encoding Fallback name : {0}",
            encoding.EncoderFallback)
        Console.WriteLine("target string : {0}", target)

        Try
            Dim bytes = encoding.GetBytes(target)
            For Each byt In bytes
                Console.Write("{0:X2} ", byt)
            Next
            Console.WriteLine()
            Console.WriteLine(
                "Decoded target : {0}",
                New String(encoding.GetChars(bytes)))
        Catch ex As EncoderFallbackException
            Console.Write("Exception Occur: ")
            If ex.IsUnknownSurrogate Then
                ' 不正なサロゲートを受け取ったらこっち
                Console.WriteLine(
                    "Unable to encode surrogate pair" & _
                    " 0x{0:X4} 0x{1:X3} at index {2}.",
                    Convert.ToUInt16(ex.CharUnknownHigh),
                    Convert.ToUInt16(ex.CharUnknownLow),
                    ex.Index)
            Else
                ' それ以外
                Console.WriteLine(
                    "Unable to encode 0x{0:X4} at index {1}.",
                    Convert.ToUInt16(ex.CharUnknown),
                    ex.Index)
            End If
        End Try
        Console.WriteLine()
    End Sub

End Module

こんなサンプルコードを参考にするくらいならmsdnのサンプルコードを見てくださいって感じですが、まぁ一応こんな感じです。 つーか弊社もmsdnのコードを参考にしました。(見覚えのあるコードがちらほら)

そんで出力がこれ

Encoding name : US-ASCII
Encoding Fallback name : System.Text.EncoderReplacementFallback
target string : hello A こんにちは
68 65 6C 6C 6F 20 3F 20 3F 3F 3F 3F 3F
Decoded target : hello ? ?????

Encoding name : US-ASCII
Encoding Fallback name : System.Text.EncoderExceptionFallback
target string : hello A こんにちは
Exception Occur: Unable to encode 0x00C5 at index 6.

Encoding name : US-ASCII
Encoding Fallback name : System.Text.EncoderReplacementFallback
target string : hello A こんにちは
68 65 6C 6C 6F 20 2A 20 2A 2A 2A 2A 2A
Decoded target : hello * *****

Encoding name : 日本語 (シフト JIS)
Encoding Fallback name : System.Text.InternalEncoderBestFitFallback
target string : hello A こんにちは
68 65 6C 6C 6F 20 41 20 82 B1 82 F1 82 C9 82 BF 82 CD
Decoded target : hello A こんにちは

Encoding name : 日本語 (シフト JIS)
Encoding Fallback name : System.Text.EncoderExceptionFallback
target string : hello A こんにちは
Exception Occur: Unable to encode 0x00C5 at index 6.

Encoding name : 日本語 (シフト JIS)
Encoding Fallback name : System.Text.EncoderReplacementFallback
target string : hello A こんにちは
68 65 6C 6C 6F 20 2A 20 82 B1 82 F1 82 C9 82 BF 82 CD
Decoded target : hello * こんにちは

us-asciiとみんな大好きShift_JISででエンコしてデコードしました。

フォールバック戦略を指定しなかった時はus-asciiは置換フォールバックを選択していますが、 Shift_JISでは最適フォールバックを選択しています。 そのため、Å(U+00C4)の『Aの上にちっさい丸がくっついている文字』*1はus-asciiでは置換フォールバックのデフォルトの置換文字である?に置き換わっています。 対してShift_JISでは最適フォールバックが選択されているため、この時は大文字のAが最適な代替と判断されそれに置き換わっています。

余談ですが、日本語版Windowsコマンドプロンプトのコードページは932(MSのShift_JISの実装)であるので、Åが正しく表現できません。 そのためか、上の出力のtarget stringの表示のÅがAになっちゃってます。 これもおそらく最適フォールバックの結果でしょう。

例外フォールバックはどちらも認識できない文字を受けとった瞬間に例外をスローしています。

置換フォールバックでは認識できない文字はデフォルトもしくは指定した文字に置き換えて出力をしています。 明示的に置換フォールバックを指定した方のShift_JISでは最適フォールバックと異なり認識できない文字は無理に代替を置かずに指定した文字に置き換えているのがわかります。

ちなみに最適フォールバックでも適切な代替がないときは置換フォールバックと同じ挙動をします。

FallbackTest(Encoding.GetEncoding(
    "Shift_JIS"), String.Format("{0}{1}", "hello Å こんにちは ", ChrW(&H2681)))
Encoding name : 日本語 (シフト JIS)
Encoding Fallback name : System.Text.InternalEncoderBestFitFallback
target string : hello A こんにちは ?
68 65 6C 6C 6F 20 41 20 82 B1 82 F1 82 C9 82 BF 82 CD 20 3F
Decoded target : hello A こんにちは ?

指定しているのはサイコロ記号*2なんですけど、どう考えてもJIS X 0208(とNEC特殊文字NEC選定IBM特殊文字IBM拡張文字)の中に適切な文字は無いんで諦めて?を表示しています。

弊社も無理難題を投げられたら例外フォールバックしたいですね。 とまぁ、フォールバックに軽く触れたので今回はここまで

*1:正式な名前は『LATIN CAPITAL LETTER A WITH DIAERESIS』

*2:U+2681『DIE FACE-2』