SJIS (Shift JIS) ファイルとして解釈可能かを判断するVBS / VBA 関数

VBSでバイナリファイルを扱えることを知り、その知識を活かしてみたいと思い、サンプルとして、あるファイルがSJISファイルとして解釈可能かを判断する VBS / VBA関数を書いてみました。

<h5>
いきさつ
</h5>

Windows10になって、メモ帳の既定の文字コードがSJISからUTF-8に変更になりました。
この結果、SJISで保存すべき業務的なファイルが意図せず、UTF-8で保存されてしまっていることが多くあるのではないかと推測します。

そこで、それを判断するために利用できる関数を書いてみました。

グローバルに言えば、この関数が Trueを返せば、そのファイルはSJISでありえますが、SJISだと断定できるわけではないです。
ただ、日本国内での使用に限定して、テキストファイルは、たいていSJISかUTF-8のどちらかであると仮定すれば、この関数がTrueを返した場合は、SJISだと判断可能だと思います。

<h5>
動作原理
</h5>

SJISは半角文字を1バイト、全角文字を2バイトで表すエンコーディングの仕組みです。
全角文字を示す2バイトの組み合わせにはルールがあり、
1バイト目は 0x81～9F または 0xE0～EF です。
2バイト目は 0x40～7F または 0x80～FC です。
そこで、この関数では、81～9F,E0～EFのデータがあったら、その次のデータが 0x40～7F ,0x80～FCになっているかを確認します。それだけです。

<h5>
考え方
</h5>

たとえば、「あ」を SJIS で保存した場合と UTF-8 で保存した場合を考えます。
「あ」は SJIS では 82 A0 です。 このバイトの並びが SJIS として解釈可能かみてみます。
先頭の1バイトである 82 は 0x81～9Fの領域に入りますので、全角文字の第1バイトであると判断できます。そこで次のバイトが 0x40～7F または 0x80～FC の領域に入っている必要がありますので、それを確認します。
次のバイトはA0ですので、0x80～FCの領域に入っています。
よって、82 A0 は SJISであると判断できます。

一方、UTF-8 では、「あ」は E3 81 82 です。
これがSJISとして判断できるかみてみます。
先頭の1バイトであるE3は 0xE0～EFの領域に入りますので、全角文字の第1バイトであると判断できます。そこで次のバイトが 0x40～7F または 0x80～FC の領域に入っている必要がありますので、それを確認します。
次のバイトは81ですので、0x80～FC の領域に入ってる要件を満たします。
ここまではOKですので、次に3バイト目に移ります。
3バイト目は82ですので、0x81～9Fの領域に入りますので、全角文字の第1バイトであると判断できます。そこで次のバイトが 0x40～7F または 0x80～FC の領域に入っている必要がありますので、それを確認します。
次のバイトはもうありません。これは、後続バイトが0x40～7F または 0x80～FC の領域に入るという要件を満たしません。
したがって、この E3 81 82 は SJISとはみなせません。

日本国内に限定すれば、SJISでなければ、UTF-8と判断してよさそうですが、
念には念を入れたい人に備えて、UTF-8のルールに則っているかの関数も
[こちら](https://qiita.com/yamashiroakihito/items/d7b23e6cd125c050d6ab) に記載しましたので、ご参照いただけると幸いです。

<h5>
実際のコード
</h5>

```vb

' あるファイルが SJISと解釈しうるファイルであるかを判断するVBS/VBA関数です。
' この関数がTrueを返すことは、SJISファイルであることの必要条件ですが、必要十分条件ではありません。

Function CanBeSJIS(TestFilePath)

    'SJISの規定に従ったファイルでないときにこの既定値で処理を抜けます。
    'VBSでは As Boolean宣言ができず、Variant型の扱いなので、既定値を明示します。
    'VBAの場合は、Function ... As Boolean で宣言しておけば、この1行は不要です。
    CanBeSJIS = False

    'ファイルをバイナリデータとして読み取るために ADODB.Streamを用います。
    Const adTypeBinary = 1
    Set Ado = CreateObject("ADODB.Stream")
    Ado.Type = adTypeBinary
    Ado.Open

    'バイトデータの並びを文字列型変数に読み取ります。
    '読み取ったデータは、配列としてはアクセスできません。
    '代わりにMidB を使って取得、代入ができます。
    Ado.LoadFromFile TestFilePath
    ByteArrayAsString = Ado.Read
    Ado.Close

    '0x81-9F または 0xE0-EF の範囲のバイトは全角文字の第1バイトです。
    '第2バイトには、0x40-7F または 0x80-FCの範囲のバイトが来ます。
    'この規則に準じているかを確認します。
    
    '文字列型の形式をとっていますが、中に入っているのはUnicode文字列ではなくByteの並びなので
    'その長さは、Lenではなく、LenBで判断します。
    For i = 1 To LenB(ByteArrayAsString)

		'MidBで各バイトにアクセス可能です。 さらにAscBに代入して、数値として扱うことができます。
		FirstByte = AscB(MidB(ByteArrayAsString,i,1))

		'全角文字列(DBCS)の先頭1バイトであるかを示すフラグです。
		IsDBCSLeading = False

		'全角文字の先頭1バイトなのかを判断します。
		'0x81-9F または 0xE0-EF の範囲のバイトは全角文字の第1バイトです。

		If &h81 <= FirstByte And FirstByte <= &h9F Then
			IsDBCSLeading = True

		ElseIf &hE0 <= FirstByte And FirstByte <= &hEF Then
			IsDBCSLeading = True
		End If

		'全角文字の第1バイトを発見したので、次のバイトが全角文字の第2バイトが確認します。
		If IsDBCSLeading Then
		'次のバイトを読むので、次回のforループがこのバイトを二度読みしないようにiを加算します。
			i = i + 1

			If i > LenB(ByteArrayAsString) Then
				'SJISとして解釈可能ではありません。読み取るデータがもうありませんでした。
				Exit Function
			End If

			'第2バイトを数値として読み取ります。
			FollowingByte = AscB(MidB(ByteArrayAsString,i,1))

			'第2バイトには、0x40-7F または 0x80-FCの範囲のバイトが来ます。
			If &h40 <= FollowingByte And FollowingByte <= &h7F Then
			'OKです。
			ElseIf &h80 <= FollowingByte And FollowingByte <= &hFC Then
			'OKです。
			Else
			'SJISとして解釈可能ではありません。
				Exit Function
			End If
		End If
	Next

    'すべてのバイトデータがSJISの規則に準じていました。
	CanBeSJIS = True
End Function

```

このコードの使い方の例

```vb
MsgBox CanBeSJIS("C:temp\test.txt")
```