Delphi 2009 と Unicode : 番外編 (結合文字列)

投稿者:: Hideaki Tominaga

概要: Delphi 2009 での Unicode における結合文字列の扱いについて解説します。

Unicode で真にやっかいなのは、結合文字列(Combining Character Sequence) と 合成文字(Composite Character)です。

このトピックでは Delphi 2009 で結合文字列を現実的な手段で扱う方法について解説します。

    結合文字列

結合文字列(Combining Character Sequence)とは、ベースとなる文字に記号等を付加して文字を構成する手段の事です。

ベトナムの文字に “ #$1EB7 ” というのがありますが、これは Unicode で U+1EB7 となります。

#$1EB7 ”という文字は、ベースとなる “ #$0061 ” という文字…これを “基底文字 (Base Character)” と言うのですが、基底文字 “ #$0061 ” の上に、お椀の様な形の “ブレーヴェ記号 ( #$0306 )” と、下点の “声調記号 ( #$0323 )” があります。この文字は Unicode で “LATIN SMALL LETTER A WITH BREVE AND DOT BELOW” と定義されています。

Unicode では “ #$1EB7 ” を ”基底文字( #$0061 ) + 声調記号( #$0323 ) + ブレーヴェ記号( #$0306 )” のような複数のコードポイントの組み合わせで表す事ができます。

以下のコードを見てみましょう。

procedure TForm1.Button1Click(Sender: TObject);
begin
  Edit1.Text := #$1EB7;                // Composite Character
  Edit2.Text := 'a' + #$0323 + #$0306; // Combining Character Sequence
end;

Hide image
Sample1

Edit1 と Edit2 には同じ文字が表示されていると思います。

基底文字と結合させて別の文字を形成できる文字を “ 結合文字(Combining Character)” 、記号を含めた形で1文字を形成している文字を “合成文字(Composite Character)” と呼びます。

上記の例では、U+1EB7 が合成文字で、“ #$0061 (U+0061)” が基底文字、“声調記号(U+0323)” と “ブレーヴェ記号 (U+0306)” が結合文字になります。そして、“ #$1EB7 ” を形成する “基底文字 + 結合文字” の組み合わせを “結合文字列” と呼びます。

    結合文字列と文字列検索

では、文字を検索してみましょう。対象となる文字列は “gap_sau_nhe” です。

この文字列から、“ #$1EB7 ” を検索してみます。

procedure TForm1.Button1Click(Sender: TObject);
var
  S: String;
begin
  // gap sau nhe
  S := #$0067#$1EB7#$0070' '#$0073#$0061#$0075' '#$006E#$0068#$00E8;
  if Pos(#$1EB7, S) > 0 then
    ShowMessage('Found.')
  else
    ShowMessage('Not Found.');
end;

この結果は当然ながらOKです。しかしながら、ファイルあるいは入力された文字列には結合文字列が含まれる可能性があります。

procedure TForm1.Button1Click(Sender: TObject);
var
  S: String;
begin
  S := Edit1.Text;
  if (Pos(#$1EB7, S) > 0) or (Pos(#$0061#$0323#$0306, S) > 0) then
    ShowMessage('Found.')
  else
    ShowMessage('Not Found.');
end;

このようにコードを変更してみました。しかしながら、このコードでは、今回のような結合文字列を正しく処理したことにはなりません。

「声調記号 (U+0323) と ブレーヴェ記号 (U+0306) が入れ替わる場合があるから?」…いえいえ、そうではありません。結合文字の並びには規則があり、“U+0061 U+0323 U+0306” というシーケンスはあっても、“U+0061 U+0306 U+0323” というシーケンスは有り得ないのです。不正なシーケンスは、通常の入力方法では実現できず、できたとしても 結合文字列として認識されないか、正常なシーケンスに矯正されてしまいます。

では、一体何が問題なのでしょうか?

実はベトナムの文字には、“ #$0061 +ブレーヴェ記号” の “#$0103 (U+0103)” や “ #$0061 +声調記号” の “#$1EA1 (U+1EA1)” があり、これらの文字に結合文字を加えて 結合文字列 “ #$1EB7 ” を形成する事も可能なのです。

Hide image
Sample2

var
  Dmy: String;
begin
  Dmy := '';

  // Composite Character
  Dmy := Dmy + 'U+1EB7: '               + #$1EB7                    + #$000D#$000A;
  Dmy := Dmy + #$000D#$000A;

  // Combining Character Sequence
  Dmy := Dmy + 'U+0061 U+0323 U+0306: ' + #$0061#$0323#$0306        + #$000D#$000A;
  Dmy := Dmy + Format('( %s + %s + %s )', [#$0061, #$0323, #$0306]) + #$000D#$000A;
  Dmy := Dmy + #$000D#$000A;

  Dmy := Dmy + 'U+0103 U+0323: '        + #$0103#$0323              + #$000D#$000A;
  Dmy := Dmy + Format('( %s + %s )',      [#$0103, #$0323])         + #$000D#$000A;
  Dmy := Dmy + #$000D#$000A;

  Dmy := Dmy + 'U+1EA1 U+0306: '        + #$1EA1#$0306              + #$000D#$000A;;
  Dmy := Dmy + Format('( %s + %s )',      [#$1EA1, #$0306]);

  ShowMessage(Dmy);
end;

上記の結果は、次のようにすべて同じ字形が表示されます。

Hide image
Combining

さて、表示上は同じに見えるこれらの文字列は、コード上では別のものです。では、これらを同じに扱って検索するために、全世界の文字の結合文字列の組み合わせを調べますか?それはあまりにも無謀というものです。

    正規化 / 標準化 (Normalize)

そこで正規化です。正規化(標準化)を難しく捉える必要はありません。今までにも、あなたは何らかの正規化を体験しているはずです。

  if UpperCase(Edit1.Text) <> UpperCase(Edit2.Text) then
    ShowMessage('Error');

これも正規化の一種です。比較対象となる2つの文字列の比較条件を何らかの方法を用いて揃える…これが “正規化” です。

Unicode における正規化には、4つの種類があります。

  • NFC (Normalization Form Canonical Composition)
    正規等価な合成文字があればそれを結合文字列に分解し、正規等価な合成文字に合成する。
  • NFD (Normalization Form Canonical Decomposition)
    正規等価な合成文字があればそれを結合文字列に分解する。
  • NFKC (Normalization Form Compatibility Composition)
    互換等価な合成文字があればそれを結合文字列に分解し、正規等価な合成文字に合成する。
  • NFKD (Normalization Form Compatibility Decomposition)
    互換等価な合成文字があればそれを結合文字列に分解する。

簡単に言えば、“正規等価” とは “見た目が全く同じ” という事で、“互換等価” とは、“ 字形は異なるが、代替が可能” という事です。

Unicode の文字列比較では、これらいずれかの正規化を前処理として行うのが前提となっています。

    Delphi 2009 で正規化を行う

残念ながら、Delphi 2009RTL には Unicode 正規化を行う関数やクラスは用意されていません。そこで、正規化を行う関数を用意してみました。

type
  // Normalization forms
  TNORM_FORM =
    (
      NormalizationOther = $00,
      NormalizationC     = $01,  // Normalization Form Canonical Composition(NFC)
      NormalizationD     = $02,  // Normalization Form Canonical Decomposition(NFD)
      NormalizationKC    = $05,  // Normalization Form Compatibility Composition(NFKC)
      NormalizationKD    = $06   // Normalization Form Compatibility Decomposition(NFKD)
    );


function MecsNormalize(const Src: WideString; var Dst: WideString; NormForm: TNORM_FORM): Boolean;
// -----------------------------------------------------------------------------
// Requires(XP/2003 or later):
// Microsoft Internationalized Domain Names (IDN) Mitigation APIs 1.1
// http://www.microsoft.com/downloads/details.aspx?FamilyID=ad6158d7-ddba-416a-9109-07607425a815&displaylang=en
// (or Internet Explorer 7 or later)
// -----------------------------------------------------------------------------
//
// Please examine U+03D3.
//  - NFC  U+03D3
//  - NFD  U+03D2 (U+0020) U+0301
//  - NFKC U+038E
//  - NFKD U+03A5 (U+0020) U+0301
//
// -----------------------------------------------------------------------------
var
  i: Integer;
  O,P :PWideChar;
  BufSize: Integer;
  FP: TFarProc;
  DLLWnd: THandle;
  // http://msdn2.microsoft.com/en-us/library/ms776395(VS.85).aspx
  NormalizeString: function(NormForm: Integer; lpSrcString: LPCWSTR; cwSrMecsLength: Integer;
    lpDstString: LPWSTR; cwDstLength: Integer):Integer; stdcall;
begin
  result := False;
  Dst := '';
  if Length(Src) = 0 then
    begin
      result := True;
      Exit;
    end;
  {$IFDEF UNICODE}
  DLLWnd := LoadLibraryW('normaliz.dll');
  {$ELSE}
  DLLWnd := LoadLibraryA('normaliz.dll');
  {$ENDIF}
  try
    FP := GetProcAddress(DLLWnd, 'NormalizeString');
    if FP <> nil then
      begin
        @NormalizeString := FP;
        BufSize := NormalizeString(Integer(NormForm), PWideChar(Src), Length(Src), nil, 0);
        if (GetLastError <> 0) then
          Exit;
        if (BufSize = 0) then
          begin
            result := True;
            Exit;
          end;
        SetLength(Dst, BufSize);
        P := PWideChar(Dst);
        for i:=1 to BufSize do
          begin
            P^ := #$0000;
            Inc(P);
          end;
        NormalizeString(Integer(NormForm), PWideChar(Src), Length(Src), PWideChar(Dst), Length(Dst));
        if (GetLastError <> 0) then
          Exit;
        P := PWideChar(Dst);
        O := P;
        while (P^ <> #$0000) do
          Inc(P);
        SetLength(Dst, P - O);
        result := True;
      end;
  finally
    if DLLWnd > 0 then
      FreeLibrary(DLLWnd);
  end;
end;

この関数はMECSUtils (http://cc.codegear.com/item/26061) から抜粋したもので、Delphi 2009 / Delphi 2007 で利用する事が可能です。詳細な利用方法は以下のリファレンスを参照して下さい。

    MecsNormalize() のリファレンス

MecsNormalize 関数はUnicode文字列を正規化(標準化)します。

Delphiの構文:

function MecsNormalize(const Src: WideString; var Dst: WideString; NormForm: TNORM_FORM): Boolean;

説明:

MecsNormalize 関数はUnicode文字列を正規化します。Src に指定されたUnicode 文字列を NormForm で指定した方法で正規化し、Dst に格納します。正規化に成功した場合には戻り値に True , 失敗した場合には False が返ります。正規化の種類については以下を参照してください。

TNORM_FORM

説明

NormalizationOther

その他の正規化 (標準化)方法。

NormalizationC

Normalization Form Canonical Composition (NFC)

NormalizationD

Normalization Form Canonical Decomposition (NFD)

NormalizationKC

Normalization Form Compatibility Composition (NFKC)

NormalizationKD

Normalization Form Compatibility Decomposition (NFKD)

注意:

MecsNormalize 関数を利用する場合、XP / Serever 2003 では“Microsoft Internationalized Domain Names (IDN) Mitigation APIs 1.1” または “Internet Explorer 7.0” 以上が必要となります。Vista 以降の OS では、特に必要とするものはありません。

    MecsNormalize() の使い方

以下のようにして使います。

var
  S1, S2: WideString;
begin  
  // Normalize
  if not MecsNormalize(Edit1.Text , S1, NormalizationD) then
    S1 := Edit1.Text;
  if not MecsNormalize(Edit2.Text , S2, NormalizationD) then
    S2 := Edit2.Text;
  // Compare
  if S1 = S2 then
    MessageBox('Equal')
  else
    MessageBox('Not Equal');
end;

例では NFD で正規化して文字列比較を行っています。

    まとめ

Unicode では、結合文字列を使おうが、合成文字を使おうが、それは自由となっています。1文字を表現するのに結合文字列を使ったとしても、それはルール違反でもなければ、特殊な事例でもありません。逆に言えば、Unicode アプリケーションではそれらを普通に扱えるようにする必要があります。

例えば、データベースアプリケーションを作る際に、データに結合文字列と合成文字が混在しそうなのであれば、検索スピードを向上させるため、または検索漏れがないように正規化を行わなくてはなりません。

…今更ですが、ベトナム語の “gap_sau_nhe” の意味を書いていませんでしたね。英語で “See you later” という意味です。では、またどこかでお会いしましょう。

次からのサーバー応答:: ETNASC03