[All]
Delphi 2009 と Unicode : 番外編 (サロゲートペア)
投稿者: : Hideaki Tominaga
概要: Delphi 2009 でのUTF-16 におけるサロゲートペアの扱いについて解説します。
Delphi 2009 と Unicode については、以前お話した通りですが、UTF-16 のサロゲートペアの詳細を知らない事には、今時の Unicode 事情は語れません。ここでは「サロゲートペアとはそもそも何?」という疑問にお答えしようと思います。
サロゲートペアの正体
UTF-16 の符号化を詳しく説明しましょう。まず、U+0000 ~ U+FFFF までの文字はBMP であり、UCS2 と同義である事は “Delphi 2009 と Unicode : Part I” でお話しました。この範囲にある文字は 1 文字を “1ワード” で表します。

このように単純に Unicode の下位16bit が格納されています。では、U+10000以上の文字はどうなるのでしょうか?

第1ワード目は “Lead Surrogate” です。15bit(最上位ビット) ~ 10bit にそれを表す “110110(0xD800)” が格納されています。続く9bit ~ 6bit 目には Unicode の上位5bit を 10進化したものから 1 を引いたものが格納されます。Unicode の上位5bit はプレーン番号を表すので、9bit ~ 6bit 目には ”プレーン番号 - 1” が格納されている事になります。5bit ~ 0bit(最下位ビット) 目にはUnicode の15bit ~ 10bit目が格納されます。
第2ワード目は “Trail Surrogate” です。最上位ビットから 6bit にそれを表す “110111(0xDC00)” が格納されています。残りのビットにはUnicode の9bit ~ 0bit (最下位ビット) が格納されています。このようにして、UTF-16 では U+10000 の文字を 2ワードで表します。
端的に言えば、コードポイント U+10000 以上にある文字を UTF-16 で符号化した場合に ”1文字=2ワード” になる、それがサロゲートペアの正体です。
Delphi 2009とサロゲートペア
MaxLength プロパティ
Unicode 文字列を GUI で入出力する際にサロゲートペアを意識する事はまずありませんが、例外があります。以下の例を見てみましょう。
procedure TForm1.Button1Click(Sender: TObject);
begin
Edi1.MaxLength := 1;
end;
このように、MaxLength プロパティに 1 を設定してしまうと、Edit1 にサロゲートペアを入力する事はできなくなります。MaxLength プロパティが制限するのは、文字数ではなく文字要素(エレメント)数だからです。
Length 関数
問題の殆どは、”文字単位で文字列処理を行う場合” に起きます。よくある間違いが Length() です。
if Length(Edit1.Text) > 10 then
begin
ShowMessage('10文字以内で入力してください');
Exit;
end;
この例では、Edit1 にサロゲートペアが含まれる文字列を入力すると、文字数制限に満たないのに、メッセージが表示されてしまいます。サロゲートペアを考慮した文字数で入力制限を行うには以下のようにします。
if SysUtils.ElementToCharIndex(Edit1.Text, Length(Edit1.Text)) > 10 then
begin
ShowMessage('10文字以内で入力してください');
Exit;
end;
Copy 関数/ AnsiMidStr 関数
次にCopy() の挙動をみてみましょう。
サロゲートペア “0xD840 0xDC0B” はコードポイント U+2000B の文字(サロゲートペア)です。
procedure TForm1.Button1Click(Sender: TObject);
var
S: String;
begin
S := 'AB' + #$D840#$DC0B + 'CD';
ShowMessage(S);
S := Copy(S, 2, 3);
ShowMessage(S);
end;
これでは正しく動作しません。何故なら、Copy() の第2引数は、文字インデックスではなく文字要素(エレメント)インデックスであり、第3引数も文字数ではなく文字要素(エレメント)数だからです。
過去にマルチバイト文字を扱った事のある方なら、「じゃあ、これでいいでしょ?」とおっしゃる事でしょう。
uses
… , AnsiStrings;
procedure TForm1.Button1Click(Sender: TObject);
var
S: String;
begin
S := 'AB' + #$D840#$DC0B + 'CD';
ShowMessage(S);
S := AnsiStrings.AnsiMidStr(S, 2, 3);
ShowMessage(S);
end;
残念ながら、UnicodeString版の AnsiMidStr() を用いても、お望みの結果にはなりません。文字インデックスと文字数による文字列操作を行いたい場合には以下のようにします。
function Copy_S(Str: UnicodeString; CharIndex: Integer; CharCount: Integer): String;
var
SIdx, EIdx : Integer;
begin
SIdx := CharToElementIndex(Str, CharIndex);
EIdx := CharToElementLen(Str, CharIndex + CharCount - 1);
result := Copy(Str, SIdx, (EIdx + 1) - SIdx);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
S: String;
begin
S := 'AB' + #$D840#$DC0B + 'CD';
ShowMessage(S);
S := Copy_S(S, 2, 3);
ShowMessage(S);
end;
サロゲートペアを処理するのに必要と思われる関数群
サロゲートペアを考慮した文字列操作に必要だと思われる関数には以下のようなものがあります。
- ByteType
- CharLength
- CharToElementIndex
- CharToElementLen
- ElementToCharIndex
- ElementToCharLen
CharLength() は補足が必要です。CharLength() は S[Index] にある文字が何バイトで構成されるか?を返します。この際に、S[Index] がサロゲートペアの第2ワードを指している場合、CharLength() は 1 を返します。
var
S: String;
Idx: Integer;
begin
S := 'AB' + #$D840#$DC0B + 'CD';
Idx := CharLength(S, 3);
ShowMessage(IntToStr(Idx));
Idx := CharLength(S, 4);
ShowMessage(IntToStr(Idx));
end;
S[Index] が BMP の文字なのか、サロゲートペアなのかを調べるには代わりに ByteType() を利用します。CharLength() の仕様で、戻り値を ”文字要素(エレメント)数” で知りたい場合には以下のようにします。
var
S: String;
Idx: Integer;
begin
S := 'AB' + #$D840#$DC0B + 'CD';
Idx := CharLength(S, 3) div StringElementSize(S);
ShowMessage(IntToStr(Idx));
Idx := CharLength(S, 4) div StringElementSize(S);
ShowMessage(IntToStr(Idx));
end;
まとめ
解かってしまえば、サロゲートペアも何てことはありません。しかし、サロゲートペアを知らないままに Unicode アプリケーションを作ってしまうと、それは “Unicode 1.0 / UCS2対応アプリケーション” にしかならない事があります。Windows Vista で普通にサロゲートペアが扱えるようになった今だからこそ、サロゲートペアと正面から向き合う姿勢が大事だと思います。
Connect with Us