VBS,VBAでの文字列データの持ち方　Unicodeからエンディアン，サロゲートペアまで

VBS/VBAでの文字列データの内部的な持ち方を確認したいと思います。

<h5>
文字コード(文字の番号)をみてみる。　「あ」
</h5>
「あ」の文字コード（文字に付けられた文字番号）を16進数で見てみます。
AscWはある文字の文字番号をみる関数、Hexは数字を16進数表現に変換する関数です。

```vb
MsgBox Hex(AscW("あ"))
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/e5c32ccd-ef55-22af-d303-9688fbbed6a8.png)

16進数で 0x3042 でした。 VBS/VBA的な表現をすれば &h3042 です。

VBSを離れて、IMEパッドの文字一覧で、「あ」を確認してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/e5a8afdc-102a-e14c-932d-168d097ef65b.png)

U+3040行の2であることがわかります。　これはその２つを足し合わせて、U+3042であることを意味しています。 VBSの&h3042と一致します。

<h5>
メモリ上のデータの並びを見てみる。　「あ」 = &h3042
</h5>


VBSは内部的に文字列をUnicodeで保持しています。
VBSにおけるUnicodeとはざっくり言って、1文字を2バイトで保持する文字列データの持ち方です。
では、VBSのメモリ上では、どういう風に2バイトで「あ」が保持されているかみてみようと思います。

「あ」が &h3042 であり 2バイトで表現されているのなら、 &h30 と &h42 に分かれて格納されているのでしょうか？  最初が&h30 で 次が&h42でしょうか？

VBSのMidBという関数は文字列をバイト単位で取り出すことができます。
この関数を使ってメモリ上の配置をみてみたいと思います。
MidB関数は、 <b>MidB(文字列,1始まりの切り出し開始バイト位置,切り出しバイト数)</b> という形式になっています。
文字の番号をみるときは、AscWを使いましたが、1バイトの値をみるときはAscBを使います。
文字は2バイトで表現されますので、1バイトよりメモリの幅が広い(Wide)です。で、AscWと覚えてください。
バイトはByteです。なので、AscBと覚えてください。
なお、AscW,AscBの「Asc」はAmerican Standard Code for Information Interchangeに由来します。
Windowsより昔の時代、アメリカにおける文字交換の標準規格をASCIIコードと言いました。らしいです。VBS/VBAの先祖であるVisual Basicのさらに先祖であるBasicでは、Ascという関数が文字コードを返しました。ここに由来します。

話がそれましたので、話を戻して「あ」のバイト単位でのメモリ配置をみてみます。
コードはこうなります。

```vb
s = "あ"  '&h3042 が文字コードです。
b1 = MidB(s,1,1) '1バイト目からの1バイトを取り出します。
b2 = MidB(s,2,1) '2バイト目からの1バイトを取り出します。

'1バイト目と2バイト目をそれぞれ16進数で表示します。
MsgBox Hex(AscB(b1)) & " " & Hex(AscB(b2))
```
結果は、こうなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/ff28d3e1-d0c4-7427-d72d-c2dce086b869.png)

なんと、予想に反して、30 42 にはならず、40 32 になります。
これは、Windowsのメモリの管理方式に起因します。

Windowsはデータの最小単位をメモリ上では下位バイトから順に格納します。
文字コード &h[XX][YY] は メモリ上、[YY] [XX] と配置されます。
このように 下位バイトから格納する方式を リトルエンディアンといいます。

<h5>
複数文字の場合のメモリの並びは? 「あい」
</h5>

では、 「あい」はどう表現されるのでしょうか？
「あ」は &h3042 、「い」は &h3044 です。
「あ」と「い」の文字の並び自体も入れ替わったりするんでしょうか？
試してみます。

```vb
s = "あい" 
b1 = MidB(s,1,1) '1バイト目からの1バイトを取り出します。
b2 = MidB(s,2,1) '2バイト目からの1バイトを取り出します。
b3 = MidB(s,3,1) '3バイト目からの1バイトを取り出します。
b4 = MidB(s,4,1) '4バイト目からの1バイトを取り出します。

'それぞれ16進数で表示します。
MsgBox Hex(AscB(b1)) & " " & Hex(AscB(b2)) & " " & Hex(AscB(b3)) & " " & Hex(AscB(b4))
```
結果はこうなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/b28fd033-05b5-ee42-273c-6294fdb4a2b7.png)

これは、こう解釈できます。
「あ」の下位バイト→「あ」の上位バイト→「い」の下位バイト→「い」の上位バイト
リトルエンディアンとは、１文字の中でのバイトの並びが逆になるだけであり、文字同士の位置関係は変わりません。文字列の見た目通りに、「い」は「あ」の次に配置されます。

<h5>
アルファベットは? 「ab」
</h5>
アルファベットはどうなるんでしょうか？
いまどきのUnicodeになれた人には、これを特別扱いする理由が思いつかないと思いますが、Unicode以前のSJISでのメモリ管理から慣れた人からすると、アルファベット等の半角文字は2バイトではなくて1バイトで管理されるもので、漢字などの全角文字は2バイトで管理されるという違いがあったので、こういう疑問を持ちます。 いまでもUTF-8は同じような発想なのかと思います。

「a」の文字コードは &h0061 , 「b」の文字コードは &h0062です。

同じく試してみます。

```vb
s = "ab" 
b1 = MidB(s,1,1) '1バイト目からの1バイトを取り出します。
b2 = MidB(s,2,1) '2バイト目からの1バイトを取り出します。
b3 = MidB(s,3,1) '3バイト目からの1バイトを取り出します。
b4 = MidB(s,4,1) '4バイト目からの1バイトを取り出します。

'それぞれ16進数で表示します。
MsgBox Hex(AscB(b1)) & " " & Hex(AscB(b2)) & " " & Hex(AscB(b3)) & " " & Hex(AscB(b4))
```

結果はこうなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/2da3c573-4d29-1c39-7994-1d06ef788f21.png)



この通り、半角文字を特別扱いすることはないです。半角文字は上位バイトが0なので
メモリ上、下位バイト→0 と並びます。

<h5>
手動でメモリ上に文字を配置してみる。　「あい」
</h5>

「あ」は U+3042 「い」は U+3044 で、メモリ上では、リトルエンディアンの影響で、&h42 &h30 &h44 &h30 であることがわかりました。

実用的な意味はありませんが、メモリ上に直接この4バイトを配置して、「あい」になるかを確認してみます。
任意の1バイトを作り出すには、 ChrB関数を使います。 このChrBは、前述のAscBの逆になります。　値からバイトを作り出せます。 
ChrはCharacterに由来します。 先に述べた大昔のBasicで この関数は値から1バイトあるいは1文字を返しました。　これがUnicodeの導入に伴って、ByteとWideに分化しました。

コードはこうなります。

```vb
s = ChrB(&h42) & ChrB(&h30) & ChrB(&h44) & ChrB(&h30)
MsgBox s
```

結果はこうなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/b8f54690-8a95-1a74-fc6d-d8448a63a9d3.png)


見事、メモリ上に直接文字列を構築できました。
ちなみに、上記のコードは、下記コードと同じ結果となります。
ChrWはメモリ内容を気にすることなく、直接、文字コードを指定できます。
こちらが、本来やるべき処理です。　上に書いたコードは学習目的でしかありません。

```vb
s = ChrW(&h3042) & ChrW(&h3044)
MsgBox s
```

<h5>
では U+FFFFより大きい文字コードは?　たとえば絵文字
</h5>

ではUnicodeで U+FFFFより大きい文字コードはどうなるんでしょうか？
たとえば、笑顔の顔文字は U+1F600です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/5974dc30-49b3-2e2c-288f-d071ddec3e65.png)

リトルエンディアンの原則から言って、00 F6 01 と格納されるんでしょうか？
でも、そもそもUnicodeは1文字を2バイトで表現するんじゃなかったんでしょうか？

試してみます。
笑顔の絵文字を変数に代入して、そのメモリの内容を16進数で表示してみます。

ただし、VBSの.vbsファイルは保存時の文字コードにUTF-8が使えません。
かといって、文字コードANSIでは絵文字等の非ANSIコードが保存できません。
<b>.vbsの保存時に「ファイルを指定して保存」で「文字コード」を「ANSI」から「UTF16-LE」に変更してください。</b>
このLEとはリトルエンディアンのことです。上ででてきた言葉が、ここにでてきました。ちなみに、「ファイル名を指定して保存」で、「UTF16-LE」の下にある「UTF16-BE」のBEはビッグエンディアンです。上位バイト→下位バイトの順にバイトを配置するやり方です。UTF16は1文字を16ビットすなわち2バイトで表すことを意味しています。

なお、私はWindows10を使っています。古いWindowsでは MsgBoxによる文字表示は絵文字に対応していないかもしれません。手元に該当OSがないので、私は試せません。
またWindows10であっても、VBSではなくてVBAはMsgBoxによる文字表示は絵文字に対応していません。

コードはこうなります。LenBとはある文字列がなんバイトあるかという関数です。こちらはLenWとLenBではなく、LenとLenBの組み合わせとなります。　LenはLengthを意味しています。

```vb
'このスクリプトは UTF16-LE で保存してください。
'絵文字の入力が困難だと思うので、このままコピペしてください。

s = "😀" 
'1バイトから最後のバイトまで1バイトづつ取り出して
'16進数に変換して、表示します。
For i = 1 To LenB(s)
    'iバイト目から1バイト取り出します。
    b = MidB(s, i, 1)
    msg = msg & " " & Hex(AscB(b))
Next
MsgBox s & msg
```
保存時の文字コード指定のやり方は、こうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/4801900b-9212-05e9-c765-c9a4452f24df.png)

結果はこうなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/34b187b0-b4db-df21-fb91-848573ee9628.png)

絵文字の文字番号である &h1F600 とは何の関連もなさそうなバイトの配列ですし、バイト数は 2でも3でもなく、想定外の4バイトでした。

上で、こう書きました。
<b>
「Unicodeとはざっくり言って、1文字を2バイトで保持する文字列データの持ち方です。」
</b>

「ざっくり言って」には意味がありました。「正確にいうと、これは正しくないですよ。」ということです。
U+FFFF以下の1文字を2バイトで、U+FFFFより大きい1文字を4バイトに分解で保持します。
後者をサロゲートペアと呼びます。
考え方としては、U+FFFFより大きい1文字をU+FFFFより小さい2文字に分解して表す考え方です。

もう1度、IMEパッドで笑顔の絵文字をみてみます。
文字カテゴリで、「追加多言語面」-「顔文字」を選び、絵顔のU+1F600にカーソルをあわせます。
ポップアップが現れ、UTF-16: 0xD83D 0xDE00 と書かれています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/83dcb93f-f575-398f-0d84-188e55c825e0.png)

つまり、U+1F600は ChrW(&hD83D) & ChrW(&hDE00) で表されます。
そして、リトルエンディアンなので、メモリ上、 3D D8 00 DE となります。
これで 先ほど表示された 3D D8 0 DE が理解できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/34b187b0-b4db-df21-fb91-848573ee9628.png)

VBS/VBAのChrW関数は、 引数に &h0000 ～ &hFFFF の範囲の数しか受け付けません。
ChrW(&h1F600) と指定しても、笑顔の絵文字を返しません。
Unicodeの範囲のU+FFFFまでしか受けつけない関数なのです。
IMEパッドのUTF-16の部分しか受けつけないともいえます。

なので、この絵文字はVBでは ChrW(&h1F600)ではなくて、ChrW(&hD83D) & ChrW(&hDE00)
となります。
やってみます。

```vb
s = ChrW(&hD83D) & ChrW(&hDE00)
MsgBox s
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/dfb9b1fa-ec6e-a329-2d26-586163fe9ba5.png)

たしかに笑顔が表示されます。

<h5>
余談 サロゲートペア文字 (U+10000以上の文字コード文字)の分解の仕方
</h5>


では、もし ChrW(&h1F600)的なことがしたければどうすればよいでしょうか？
これは、UnicodeのU+10000以上の範囲をどうすれば、サロゲートペア文字の組み合わせに変換できるかという話になります。
これはVBSの話というよりは、Unicodeの話になります。
VBS単体では、絵文字などは4バイトで保持されると覚えておけば十分ではあります。
ご興味のある方は、この先も引き続き読んでいただけるとうれしいです。

U+10000以上の文字は2文字のペアとなるサロゲート文字に分解されます。
分解してできた1文字目は 上位サロゲートと呼ばれ、D800～DBFFの1文字を使います。
分解してできた2文字目は 下位サロゲートと呼ばれ、DC00～DFFFの1文字を使います。
分解の仕方としては、文字コードから &h10000を引いて、サロゲートペア範囲内でのインデックスを求めて、そのインデックスの上位10ビットをD800～DBFFの範囲に割り当てます。さらに、同じそのインデックスの下位10ビットをDC00～DFFFに割り当てます。

コードとしてはこうなります。

```vb

MsgBox CodeToSurrogatesPair(&h1F600)

Function CodeToSurrogatesPair(ByVal Code)

	'Code引数を更新しているので、呼び出し元に副作用をあたえないように
	'ByVal宣言を付けて、値渡しにします

	'&h8000～&hFFFFは、&h8000&のように後ろに&を付けないと
	'Interger型と解釈され、負数に解釈されます。
	'これを防ぐために、16進数に再変換して、CLngでLong型として再解釈して
	'負数に解釈されるのを防ぎます。
	Code = CLng("&h" & Hex(Code))

	If Code <= &hFFFF Then
		CodeToSurrogatesPair = ChrW(Code)
		Exit Function
	End If		
	
	'vbs/vbaは二進数数値表現ができないので、変数名で二進数を表します。
	Const b11111111110000000000 = &HFFC00&
	Const b00000000001111111111 = &H3FF&
	Const b00000000010000000000 = &H400&
    
	'サロゲートペアとは、U+10000以上のものをU+10000以上の範囲内での
	'0始まりのインデックスを求め
	'そのインデックスの上位10ビットをD800～DBFF
	'そのインデックスの下位10ビットをDC00～DFFF に割り振ったものです。
 
	IndexInSurrogates = Code - &H10000
        
	'And演算子で、上位10ビットを取得し、2進数の10000000000で割ることで
	'右に10ビットシフトします。
	'さらにD800を加算することで、D800～DBFFの範囲にずらします。     
	High = (IndexInSurrogates And b11111111110000000000) _
		/ b00000000010000000000 + &HD800&

	'And演算子で、下位10ビットを取得し、DC00を加算することで
	'DC00～DF00の範囲にずらします。
	Low  = (IndexInSurrogates And b00000000001111111111) + &HDC00&

	CodeToSurrogatesPair = ChrW(High) & ChrW(Low)

End Function

```

実行してみます。結果はこうなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/274ef0ec-cd0b-0ba6-72b3-7be601c2c456.png)

以上、VBS,VBAでの文字列データの持ち方を考察してみました。