VBS/VBAでの文字列データの内部的な持ち方を確認したいと思います。
文字コード(文字の番号)をみてみる。 「あ」
「あ」の文字コード(文字に付けられた文字番号)を16進数で見てみます。 AscWはある文字の文字番号をみる関数、Hexは数字を16進数表現に変換する関数です。MsgBox Hex(AscW("あ"))
16進数で 0x3042 でした。 VBS/VBA的な表現をすれば &h3042 です。
VBSを離れて、IMEパッドの文字一覧で、「あ」を確認してみます。
U+3040行の2であることがわかります。 これはその2つを足し合わせて、U+3042であることを意味しています。 VBSの&h3042と一致します。
メモリ上のデータの並びを見てみる。 「あ」 = &h3042
VBSは内部的に文字列をUnicodeで保持しています。
VBSにおけるUnicodeとはざっくり言って、1文字を2バイトで保持する文字列データの持ち方です。
では、VBSのメモリ上では、どういう風に2バイトで「あ」が保持されているかみてみようと思います。
「あ」が &h3042 であり 2バイトで表現されているのなら、 &h30 と &h42 に分かれて格納されているのでしょうか? 最初が&h30 で 次が&h42でしょうか?
VBSのMidBという関数は文字列をバイト単位で取り出すことができます。
この関数を使ってメモリ上の配置をみてみたいと思います。
MidB関数は、 MidB(文字列,1始まりの切り出し開始バイト位置,切り出しバイト数) という形式になっています。
文字の番号をみるときは、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という関数が文字コードを返しました。ここに由来します。
話がそれましたので、話を戻して「あ」のバイト単位でのメモリ配置をみてみます。
コードはこうなります。
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))
なんと、予想に反して、30 42 にはならず、40 32 になります。
これは、Windowsのメモリの管理方式に起因します。
Windowsはデータの最小単位をメモリ上では下位バイトから順に格納します。
文字コード &h[XX][YY] は メモリ上、[YY] [XX] と配置されます。
このように 下位バイトから格納する方式を リトルエンディアンといいます。
複数文字の場合のメモリの並びは? 「あい」
では、 「あい」はどう表現されるのでしょうか?
「あ」は &h3042 、「い」は &h3044 です。
「あ」と「い」の文字の並び自体も入れ替わったりするんでしょうか?
試してみます。
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))
これは、こう解釈できます。
「あ」の下位バイト→「あ」の上位バイト→「い」の下位バイト→「い」の上位バイト
リトルエンディアンとは、1文字の中でのバイトの並びが逆になるだけであり、文字同士の位置関係は変わりません。文字列の見た目通りに、「い」は「あ」の次に配置されます。
アルファベットは? 「ab」
アルファベットはどうなるんでしょうか? いまどきのUnicodeになれた人には、これを特別扱いする理由が思いつかないと思いますが、Unicode以前のSJISでのメモリ管理から慣れた人からすると、アルファベット等の半角文字は2バイトではなくて1バイトで管理されるもので、漢字などの全角文字は2バイトで管理されるという違いがあったので、こういう疑問を持ちます。 いまでもUTF-8は同じような発想なのかと思います。「a」の文字コードは &h0061 , 「b」の文字コードは &h0062です。
同じく試してみます。
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))
結果はこうなります。
この通り、半角文字を特別扱いすることはないです。半角文字は上位バイトが0なので
メモリ上、下位バイト→0 と並びます。
手動でメモリ上に文字を配置してみる。 「あい」
「あ」は U+3042 「い」は U+3044 で、メモリ上では、リトルエンディアンの影響で、&h42 &h30 &h44 &h30 であることがわかりました。
実用的な意味はありませんが、メモリ上に直接この4バイトを配置して、「あい」になるかを確認してみます。
任意の1バイトを作り出すには、 ChrB関数を使います。 このChrBは、前述のAscBの逆になります。 値からバイトを作り出せます。
ChrはCharacterに由来します。 先に述べた大昔のBasicで この関数は値から1バイトあるいは1文字を返しました。 これがUnicodeの導入に伴って、ByteとWideに分化しました。
コードはこうなります。
s = ChrB(&h42) & ChrB(&h30) & ChrB(&h44) & ChrB(&h30)
MsgBox s
結果はこうなります。
見事、メモリ上に直接文字列を構築できました。
ちなみに、上記のコードは、下記コードと同じ結果となります。
ChrWはメモリ内容を気にすることなく、直接、文字コードを指定できます。
こちらが、本来やるべき処理です。 上に書いたコードは学習目的でしかありません。
s = ChrW(&h3042) & ChrW(&h3044)
MsgBox s
では U+FFFFより大きい文字コードは? たとえば絵文字
ではUnicodeで U+FFFFより大きい文字コードはどうなるんでしょうか?
たとえば、笑顔の顔文字は U+1F600です。
リトルエンディアンの原則から言って、00 F6 01 と格納されるんでしょうか?
でも、そもそもUnicodeは1文字を2バイトで表現するんじゃなかったんでしょうか?
試してみます。
笑顔の絵文字を変数に代入して、そのメモリの内容を16進数で表示してみます。
ただし、VBSの.vbsファイルは保存時の文字コードにUTF-8が使えません。
かといって、文字コードANSIでは絵文字等の非ANSIコードが保存できません。
.vbsの保存時に「ファイルを指定して保存」で「文字コード」を「ANSI」から「UTF16-LE」に変更してください。
この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を意味しています。
'このスクリプトは 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
絵文字の文字番号である &h1F600 とは何の関連もなさそうなバイトの配列ですし、バイト数は 2でも3でもなく、想定外の4バイトでした。
上で、こう書きました。
「Unicodeとはざっくり言って、1文字を2バイトで保持する文字列データの持ち方です。」
「ざっくり言って」には意味がありました。「正確にいうと、これは正しくないですよ。」ということです。
U+FFFF以下の1文字を2バイトで、U+FFFFより大きい1文字を4バイトに分解で保持します。
後者をサロゲートペアと呼びます。
考え方としては、U+FFFFより大きい1文字をU+FFFFより小さい2文字に分解して表す考え方です。
もう1度、IMEパッドで笑顔の絵文字をみてみます。
文字カテゴリで、「追加多言語面」-「顔文字」を選び、絵顔のU+1F600にカーソルをあわせます。
ポップアップが現れ、UTF-16: 0xD83D 0xDE00 と書かれています。
つまり、U+1F600は ChrW(&hD83D) & ChrW(&hDE00) で表されます。
そして、リトルエンディアンなので、メモリ上、 3D D8 00 DE となります。
これで 先ほど表示された 3D D8 0 DE が理解できました。
VBS/VBAのChrW関数は、 引数に &h0000 ~ &hFFFF の範囲の数しか受け付けません。
ChrW(&h1F600) と指定しても、笑顔の絵文字を返しません。
Unicodeの範囲のU+FFFFまでしか受けつけない関数なのです。
IMEパッドのUTF-16の部分しか受けつけないともいえます。
なので、この絵文字はVBでは ChrW(&h1F600)ではなくて、ChrW(&hD83D) & ChrW(&hDE00)
となります。
やってみます。
s = ChrW(&hD83D) & ChrW(&hDE00)
MsgBox s
たしかに笑顔が表示されます。
余談 サロゲートペア文字 (U+10000以上の文字コード文字)の分解の仕方
では、もし 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に割り当てます。
コードとしてはこうなります。
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
実行してみます。結果はこうなります。
以上、VBS,VBAでの文字列データの持ち方を考察してみました。