1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-19

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

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

image.png

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

VBSを離れて、IMEパッドの文字一覧で、「あ」を確認してみます。
image.png

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))

結果は、こうなります。
image.png

なんと、予想に反して、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))

結果はこうなります。
image.png

これは、こう解釈できます。
「あ」の下位バイト→「あ」の上位バイト→「い」の下位バイト→「い」の上位バイト
リトルエンディアンとは、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))

結果はこうなります。

image.png

この通り、半角文字を特別扱いすることはないです。半角文字は上位バイトが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

結果はこうなります。

image.png

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

s = ChrW(&h3042) & ChrW(&h3044)
MsgBox s
では U+FFFFより大きい文字コードは? たとえば絵文字

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

image.png

リトルエンディアンの原則から言って、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

保存時の文字コード指定のやり方は、こうです。
image.png

結果はこうなります。
image.png

絵文字の文字番号である &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 と書かれています。
image.png

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

image.png

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

image.png

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

余談 サロゲートペア文字 (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

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

image.png

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?