2023年12月25日

文字列を書記素クラスタで分割する

このアーティクルはDelphi Advent Calendar 2023の25日目の記事です(12日ぶり11回目)。

Unicodeの世界では、1つの"文字"(書記素、grapheme)が1つのコードポイント(code point)で表されるとは限りません。サロゲートペアとか結合文字とか絵文字とか、文字列に入っているものがどれくらいの"文字数"になっているのかを知るのは意外に難しい話です。
現在一般的と考えられる方法として、がありますが、ICUはインタフェースがUTF-8ベースでDelphiからは使いにくく、Delphiの正規表現ライブラリはPCRE(1) 8.45ベースでだいぶ古くてUnicodeの新しいバージョンには対応していません(RSP-42524)。また自前での実装は相当面倒なうえに、Unicodeのバージョンアップに追従していくのが大変です。
ところがDelphi 12で標準サポートされたSkia4Delphiでは簡単に文字列を書記素クラスタで分割できるようになりました(ICUを取り込んでいるようです)。試しに正規表現(PCRE)による分割と比較してみましょう。
なおDelphi 11 Alexandriaおよびそれ以前のバージョンではSkia4Delphiをインストールする必要があります(現時点(2023/12)の最新は6.0.0)。

新規プロジェクトを作成し、フォーム(フォントサイズを大きめにしておくと結果が見やすくなります)にTEditを1つ、TButtonを2つ、結果表示のためのTMemoを1つ配置します。またプロジェクトツールウィンドウでプロジェクトを右クリック→Skiaを有効化を選択して、実行ファイルと同じ場所にsk4d.dllが配置されるようにします。
usesにSystem.Skiaを追加し、フォームのOnShowイベントで
procedure TForm1.FormShow(Sender: TObject);
begin
  Edit1.Text := #$20BB7 + '野屋のコピペ' +
                #$00E5 + #$00E1 + #$0302 + #$0303 + #$0304 +
                #$1F62D +
                #$1F937 + #$1F3FD + #$200D + #$2640 + #$FE0F +
                #$1F468 + #$200D + #$1F469 + #$200D + #$1F467 + #$200D + #$1F466 +
                #$1F469 + #$1F3FD + #$200D + #$1F4BB +
                #$1F1EF + #$1F1F5;
end;
とEdit1.Textにサンプル文字列を設定し、Button1のOnClickイベントで
procedure TForm1.Button1Click(Sender: TObject);
begin
  for var Match in TRegEx.Matches(Edit1.Text,'\X') do
  begin
    Memo1.Lines.Add(Match.Value);
  end;
end;
Button2のOnClickイベントで
procedure TForm1.Button2Click(Sender: TObject);
var
  SkUnicode: ISkUnicode;
begin
  SkUnicode := TSkUnicode.Create;
  for var S in SkUnicode.GetBreaks(Edit1.Text,TSkBreakType.Graphemes) do
  begin
    Memo1.Lines.Add(S);
  end;
end;
とします。では実行してみましょう。まず正規表現です。
𠮷
野
屋
の
コ
ピ
ペ
å
á̂̃̄
😭
🤷
🏽‍
♀️
👨‍
👩‍
👧‍
👦
👩
🏽‍
💻
🇯🇵
(Windows上の表示とは若干異なります)
正規表現による分割では絵文字のZWJ(ゼロ幅接合子)による結合はうまく処理できないようです。これはPCRE 8.45がだいぶ古いバージョンで、最新のUnicodeに対応できていないことが原因と考えられます。

次にSkia4Delphiです。
𠮷
野
屋
の
コ
ピ
ペ
å
á̂̃̄
😭
🤷🏽‍♀️
👨‍👩‍👧‍👦
👩🏽‍💻
🇯🇵
一方でSkia4Delphiによる分割は正しく処理できていますね。ちなみにWindowsでは国旗の絵文字は絵文字としてではなくRegional indicator symbol2文字での表現になるようです(サンプル最後の"🇯🇵"が"JP"となる)。

書記素クラスタによる分割は文字列のレンダリングに必須なためSkia(Skia4Delphi)に実装されていると考えられますが、プログラムで書記素クラスタによる分割が必要になるのは(帳票などで)文字列がどのくらいの幅を占めるかを知りたいときではないでしょうか。そのためには書記素クラスタによる分割だけでなく、Unicode Standard Annex #11 East Asian Widthも必要になります。これはDelphi標準のSystem.CharacterユニットにもSkia4Delphiにも実装されておらず、EastAsianWidth.txtを取り込んで自前で実装する必要があります。これについては後日別のアーティクルで扱うことにします。

Skia4Delphを使って見やすくしたバージョンをGistに上げてあります。

0 件のコメント: