Delphi 2009とUnicode:番外編(サロゲートペア)

提供: Support
移動先: 案内検索

この記事は以前EDNサイトに作成されていた記事を転載したものです。

概要

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ワード」で表します。

D2009 uni sp 1.png

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

D2009 uni sp 2.png

第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); // 4 (バイト単位)
  ShowMessage(IntToStr(Idx));
  Idx := CharLength(S, 4); // 2 (バイト単位)
  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); // 2 (エレメント単位)
  ShowMessage(IntToStr(Idx));
  Idx := CharLength(S, 4) div StringElementSize(S); // 1 (エレメント単位)
  ShowMessage(IntToStr(Idx));
end;

まとめ

解かってしまえば、サロゲートペアも何てことはありません。しかし、サロゲートペアを知らないままにUnicodeアプリケーションを作ってしまうと、それは「Unicode 1.0/UCS2対応アプリケーション」にしかならない事があります。Windows Vistaで普通にサロゲートペアが扱えるようになった今だからこそ、サロゲートペアと正面から向き合う姿勢が大事だと思います。

関連記事