質問: Javaで文字列の「人間にとっての文字数」を数えるにはtoString()ではダメです。サロゲートペア文字が2文字にカウントされてしまいます。codePointCount()も惜しいのですがダメです。異体字セレクタ込みの文字や互換分解された文字が複数文字にカウントされてしまいます。 ではどうするかというとBreakIterator.getCharacterInstanceを使って人間にとっての文字をイテレートしてもらうというのが正解になるのですが、このメソッド、Localeを引数に取ります。 同じクラスのgetSentenceInstance(文を列挙)とかgetWordInstance(単語を列挙)とかなら思い切りロケール依存しますんでロケールを引数に取るのもわかるのですが、getCharacterInstanceはロケールをどのように使うのでしょうか。 そして、より直接的な質問の目的なのですが、言語非依存なサーバプログラムを書こうというときにはなんのロケールを指定すべきなんでしょうか。 (getAvailableLocalesの仕様を呼んでいると、USを指定するのが確実なのかとも思いますが)
回答: ### getCharacterInstanceはロケールをどのように使うのでしょうか。 `getCharacterInstance`メソッドの戻り値である`BreakIterator`クラスのAPIリファレンスにある、以下の記述が回答になるかと思います。 > > 返される境界は、補助文字、結合文字シーケンス、または合字クラスタの境界になる場合があります。たとえば、アクセント付きの文字は、基準文字と発音区別符号として格納されている場合があります。ユーザーの文字に対する認識は言語間で異なります。 > > > で、ウィキペディアの"合字"に関する項を読んでみると、以下のように書いてあります。 > > 合字(ごうじ)またはリガチャー(英: Ligature)とは、複数の文字を合成して一文字にしたもの。 > > (中略) > > インド系文字のほとんどは複数の文字を合成して一音節の音(言語によっては複数の音節で読まれる)を表す字を作るシステムになっている。文字コード上では合字は一部のもの(ॐ などの表意文字として機能する字等)を除いて単独の文字として存在せず、複数の特定の文字を決まった順番で並べた際に1文字の合字として扱われるシステムになっている。 > > > このような文字を正しく「1文字」と判定するためには、やはりロケールを指定してやる必要があるのではないでしょうか? なぜなら、例えば 「複数のロケールで使用されている文字体系の中に、ある特定のロケールにだけ存在する合字」 とうものも、可能性としてあり得ると思うからです。 ### 言語非依存なサーバプログラムを書こうというときにはなんのロケールを指定すべきなんでしょうか。 上記の理由から、これには正解は無いように思います。 `getAvailableLocales`メソッドの > > これには、Locale.USと等価なLocaleインスタンスが少なくとも1つ含まれている必要があります。 > > > という記述も、おそらくJavaランタイムが最低限、サポートしなければならないロケールが`en_US`だからではないかと推測します。 > > there is no requirement that all runtime implementations support the same set of locales. But all implementations must support a minimal list of them. This list is quite short: English (U.S.). > > > ``` すべてのランタイム実装は同じロケールのセットをサポートしている必要はありません。 しかし、すべての実装は最小限のリストをサポートしている必要があります。 このリストは非常に短いです:英語(米国) ``` ちなみに、本回答にあたり`getCharacterInstance`メソッドのソースコードを読んでみようとしましたが、私には理解不可能でした(^^; 2016/07/11 追記 ============== 他のものと異なる文字数をカウントするロケールを検出するプログラム。 `-enableassertions`オプションを付けて実行してください。 ``` import java.text.BreakIterator; import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.Map; public class Main { private static final Map TESTER; static { Map tester = new HashMap(); tester.put("Julius Cæsar", 12); // 合字 tester.put("\u0075\u0308\u0304", 1); // 合字 tester.put("\u30DB\u309A", 1); // 合字 tester.put("株式会社", 1); // 合字 tester.put("キャリーパミュパミュ", 12); // 半角カナ tester.put("ドル亞䖸丿塔", 5); // 旧字・異字体 tester.put("कवि की उमंग उल्का मुट्ठी", 14); // ヒンドゥー後 tester.put( "\u05DC\u05B4\u05D4\u05B0\u05D9\u05D5\u05B9\u05EA\u0020\u05E2\u05B7\u05DD\u0020\u05D7\u05B8\u05E4\u05B0\u05E9\u05B4\u05C1\u05D9\u0020\u05D1\u05B0\u05BC\u05D0\u05B7\u05E8\u05B0\u05E6\u05B5\u05E0\u05D5\u05BC\u0020\u05D0\u05B6\u05E8\u05B6\u05E5\u0020\u05E6\u05B4\u05D9\u05BC\u05D5\u05B9\u05DF\u0020\u05D5\u05B4\u05D9\u05E8\u05D5\u05BC\u05E9\u05B8\u05C1\u05DC\u05B7\u05D9\u05B4\u05DD", 38); // ヘブライ語、右から読む TESTER = Collections.unmodifiableMap(tester); } private static final String ERROR_MESSAGE = "Locale=%s, text=%s, expected=%d, detected=%d"; public static void main(String[] args) { for (Map.Entry tester : TESTER.entrySet()) { for (Locale locale : Locale.getAvailableLocales()) { int count = countCharactor(tester.getKey(), locale); assert count == tester.getValue() : String.format(ERROR_MESSAGE, locale.toLanguageTag(), tester.getKey(), tester.getValue(), count); } } } private static int countCharactor(String text, Locale locale) { BreakIterator iterator = BreakIterator.getCharacterInstance(locale); iterator.setText(text); int count = 0; while (iterator.next() != BreakIterator.DONE) { count++; } return count; } } ```