UTF-8ファイルとして解釈可能かを判断するVBS / VBA 関数

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

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

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

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

<h5>
原理
</h5>
UTF-8はすべての文字を1バイトから4バイトの可変長のバイトの組み合わせで表すエンコーディングの仕組みです。
各組合せが何バイトなのかは、各組合せの1バイト目で識別できます。
1バイト目が00～7Fであれば、その組み合わせは1バイトで構成されます。
1バイト目がC2～DFであれば、その組み合わせは2バイトで構成されます。
1バイト目がE0～EFであれば、その組み合わせは3バイトで構成されます。
1バイト目がF0～F4であれば、その組み合わせは4バイトで構成されます。
そして、各組合せの2バイト目以降は、80～BFの間のデータのみで構成されます。

このルールに則っているかを判断します。

<h5>
考え方
</h5>

たとえば、「あ」を UTF-8 で保存した場合と SJIS で保存した場合を考えます。
「あ」は UTF-8 では、E3 81 82 です。
先頭の1バイトであるE3は E0～EFのデータなので、3バイトで構成される組み合わせとなります。その後続には2バイトの80～BFのデータが続く必要があります。
実際、2バイト目の81と3バイト目の82は、80～BFの間に収まっています。
そこで、E3 81 82 は UTF-8として判断できます。

「あ」は SJIS では 82 A0 です。 このバイトの並びが UTF-8 として解釈可能かみてみます。
先頭の1バイトである 82 は 00～7Fの領域に入りませんし、C2～DFの領域にも入りません。
このバイトの組み合わせはUTF-8のルールに則っていません。
日本国内に限定すれば、UTF-8でなければ、SJISと判断してよさそうですが、
念には念を入れたい人に備えて、SJISのルールに則っているかの関数も
[こちら] (https://qiita.com/yamashiroakihito/items/eb03e5cb7a2a7a6c21b1)に記載しましたので、ご参照いただけると幸いです。

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

```vb
' あるファイルが UTF8と解釈しうるファイルであるかを判断するVBS/VBA関数です。
' 英字と半角カタカナ等を組みあわせた SJISファイルの場合には、SJISともUTF-8とも
' 解釈しうる可能性がありますので、IsUTF8ではなく、CanBeUTF8としました。
' この関数がTrueを返すことは、UTF-8ファイルであることの必要条件ですが、必要十分条件ではありません。

Function CanBeUTF8(TestFilePath)

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

	'1バイト目	2バイト目以降
	'00..7F		なし
	'C2..DF		80..BF
	'E0..EF		80..BF		80..BF
	'F0..F4		80..BF		80..BF		80..BF

	'文字列型の形式をとっていますが、中に入っているのはUnicode文字列ではなくByteの並びなので
	'その長さは、Lenではなく、LenBで判断します。
	For i = 1 To LenB(ByteArrayAsString)

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

		'1バイト目の値によって、２バイト目以降が何バイトあるかが決まります。
		If &h00 <= FirstByte And FirstByte <= &h7F Then
			FollowingBytesCount = 0

		ElseIf &hC2 <= FirstByte And FirstByte <= &hDF Then
			FollowingBytesCount = 1

		ElseIf &hE0 <= FirstByte And FirstByte <= &hEF Then
			FollowingBytesCount = 2

		ElseIf &hF0 <= FirstByte And FirstByte <= &hF4 Then
			FollowingBytesCount = 3
		Else
			'UTF-8として解釈可能ではありません。
			Exit Function
		End If

		'2バイト目以降が決められたバイトの数だけ 80-BFの間の値で続いているかを確認します。
		For j = 1 To FollowingBytesCount
			i = i + 1

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

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

			'後続バイトは 0x80 ～ 0xBF の範囲のバイトでなければなりません。	 
			If &h80 <= FollowingByte And FollowingByte <= &hBF Then
			Else
				'UTF-8として解釈可能ではありません。
				Exit Function
			End If
		Next
		'ここに来た時、変数iは jのループの中で後続バイト数の分加算されているので
		'次のバイトの組み合わせの直前のバイト位置を指し示しています。
	Next
	'すべてのバイトデータがUTF-8の規則に準じていました。
	CanBeUTF8 = True
End Function

```

このコードの使い方の例

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