Java17(Java13以降)でgraphemeをカウントする

Java17(Java13以降)でgraphemeをカウントする

Java17(Java13以降)でgraphemeを簡単にカウント出来るようになっていたのでそのメモです。

下記のtweetを見て、Java13以降ではgraphemeを簡単にカウント出来るようになっている情報を知ったので
今更試してみました。

下記バージョンで試してみます。

dependency

gradleを使います。

比較に使用するためにICUのBreakIteratorを追加します。

build.gradle

dependencies {
    implementation("com.ibm.icu:icu4j:71.1")
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
    }
}

grapheme

今までJavaでgraphemeをカウントするにはBreakIteratorを使用していました。
さらに、java.textのBreakIteratorは絵文字を正しくカウント出来ないので、 ICUのBreakIteratorの実装を使用していました。

上記のtweetでJava13以降では簡単にgraphemeをカウント出来るようになったので確認してみます。

下記の3パターンでgraphemeを確認してみます。

  • java.text.BreakIterator
  • com.ibm.icu.text.BreakIterator
  • java13+

java.text.BreakIterator

java.text.BreakIterator で grapheme をカウントするために下記のメソッドを用意しておきます。

public static int getGraphemeLength(String value) {
    final BreakIterator it = BreakIterator.getCharacterInstance();
    it.setText(value);
    int count = 0;
    while (it.next() != BreakIterator.DONE) {
        count++;
    }
    return count;
}

com.ibm.icu.text.BreakIterator

com.ibm.icu.text.BreakIterator で grapheme をカウントするために下記のメソッドを用意しておきます。
Java だと別名 import が出来ないので、完全修飾名で使用します。

public static int getGraphemeLengthWithIcu(String value) {
    final com.ibm.icu.text.BreakIterator it = com.ibm.icu.text.BreakIterator.getCharacterInstance();
    it.setText(value);
    int count = 0;
    while (it.next() != BreakIterator.DONE) {
        count++;
    }
    return count;
}

Java13+

tweetにあったようにJava13以降では下記のように\b{g}を利用します。
Oracleのドキュメントを見るとA Unicode extended grapheme cluster boundary(Unicode拡張書記素クラスタ境界)となっています。
https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/util/regex/Pattern.html

str.split("\\b{g}").length

確認

下記のメインコードで確認してみます。
👨‍👨‍👦の絵文字をカウントしてみます。

Main.java

public static void main(String[] args) {
    // 👨‍👨‍👦
    final String str = "\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC66";

    System.out.println(str);
    System.out.println("length: " + str.length());
    System.out.println("BreakIterator(java): " + getGraphemeLength(str));
    System.out.println("BreakIterator(ICU): " + getGraphemeLengthWithIcu(str));
    System.out.println("java13+: " +  str.split("\\b{g}").length);
}

結果。
Java13+とICUのBreakIteratorではうまくカウント出来ています。
やはりjava.text.BreakIteratorはうまくカウント出来ませんでした。

👨‍👨‍👦
length: 8
BreakIterator(java): 5
BreakIterator(ICU): 1
java13+: 1

追記

この記事を書いたあとに maki さんの tweet で skin tone のカウントがおかしいというのを見かけて試してみました。

同様に下記のコードで確認します。

Main.java

public static void main(String[] args) {
    // a + 🏻
    final String aAndSkinTone = "a" + "\uD83C\uDFFB";
    System.out.println(aAndSkinTone);
    System.out.println("length: " + aAndSkinTone.length());
    System.out.println("BreakIterator(java): " + getGraphemeLength(aAndSkinTone));
    System.out.println("BreakIterator(ICU): " + getGraphemeLengthWithIcu(aAndSkinTone));
    System.out.println("java13+: " + aAndSkinTone.split("\\b{g}").length);
}

ICU の BreakIterator でも結果は同じでした。

a🏻
length: 3
BreakIterator(java): 2
BreakIterator(ICU): 1
java13+: 1

おわり。

サンプルコードは下記にあげました。

github.com

[参考]

文字数をカウントする7つの方法 - LINE ENGINEERING