2017年3月13日月曜日

Javaで、文字列を一文字ずつに分解する (サロゲートペアや結合文字を考慮)

まとめ

Javaの標準APIで実現できる。
java.text.BreakIterator.getCharacterInstance() を使用すればよい。

なお、以下の文章における「文字」という語は、「書記素クラスター」(grapheme clusters) を意味している。

要件

  • Javaの文字列 (UTF-16) を、「一文字ずつ」に分解(分割)したい。
  • ただし、String#toCharArray() は使用できない。
    理由: UTF-16 で扱う文字の中には、単一の char (16ビット) に収まらない文字が存在するため。もし String#toCharArray() を使用してしまうと、一つの文字が 複数の char値 に分断されてしまう。
  • たとえば、U+20BB7 (𠮷) は一文字だが、格納するためには char型の変数が2つ必要。
    char[] u_20bb7_tsuchiyoshi = { '\ud842', '\udfb7' }; /* 𠮷 */
    ※コードポイント (= UNICODE上での文字コード) が U+0FFFF より大きい文字は、
    サロゲートペア (Surrogate pair) を用いて表現される。
  • さらに、単一の文字を、複数 (2つ以上) のコードポイントの組み合わせで表現する場合もある。
  • たとえば、 は、
    一つの文字ではあるが 以下の 5つのコードポイントを結合した結果である。
    • U+0061 ( a ) LATIN SMALL LETTER A
    • U+0308 (  ̈ ) COMBINING DIAERESIS
    • U+0303 (  ̃ ) COMBINING TILDE
    • U+0323 (  ̣ ) COMBINING DOT BELOW
    • U+032D (  ̭ ) COMBINING CIRCUMFLEX ACCENT BELOW

(参考) BreakIterator による、文字列→文字への分解 ※無保証

public static List<String> graphemeClusters(String str) {
  final List<String>  result          = new ArrayList<String>(str.length());

  final BreakIterator breakIterator   = BreakIterator.getCharacterInstance();
  breakIterator.setText(str);

  int start = breakIterator.first();
  int end   = breakIterator.next();

  while (end != BreakIterator.DONE) {
      result.add(str.substring(start, end));

      start = end;
      end   = breakIterator.next();
  }
  return result;
}
/* 実行結果
graphemeClusters("abc")
= [ "a", "b", "c" ]
= [ \u0061, \u0062, \u0063 ]

graphemeClusters("あいう")
= [ "あ", "い", "う" ]
= [ \u3042, \u3044, \u3046 ]

graphemeClusters("𠮷野家")
= [ "𠮷", "野", "家" ]
= [ \ud842 \udfb7, \u91ce, \u5bb6 ]

graphemeClusters("葛󠄁飾区")
= [ "葛󠄁", "飾", "区" ]
= [ \u845b \udb40 \udd01, \u98fe, \u533a ]

graphemeClusters("Åström")
= [ "Å", "s", "t", "r", "ö", "m" ]
= [ \u0041 \u030a, \u0073, \u0074, \u0072, \u006f \u0308, \u006d ]

graphemeClusters("ạ̭̈̃")
= [ "ạ̭̈̃" ]
= [ \u0061 \u0308 \u0303 \u0323 \u032d ]
*/

参考