これは、なにをしたくて書いたもの?
前に、Java 11でこんなエントリーを書きました。
この時の主なテーマはBreakIterator
だったのですが、BreakIterator
ではうまく扱えない絵文字があることがわかりまして。
その後、Java 13以降であればUnicode拡張書記素クラスタ境界(正規表現)を使うと扱えることがわかり、今回こちらを試してみることに
しました。
BreakIteratorでうまく扱えない絵文字
たとえば、以下のようなものです。
絵文字 | 数値文字参照 |
---|---|
👆🏼 | 👆🏼 |
🇦🇿 | 🇦🇿 |
🏴 | 🏴󠁧󠁢󠁥󠁮󠁧󠁿 |
👨👩👧👦 | 👨‍👩‍👧‍👦 |
複数の絵文字・文字で、ひとつの絵文字を形成するような文字です。
JavaとUnicodeのバージョン
JavaがサポートしているUnicodeのバージョンですが、Character
のJavadocに書いてありました。
Character (Java SE 17 & JDK 17)
Java 17の時点でUnicode 13.0をサポートしています。
現在のUnicodeは15.0です。
この中に、Emoji 15.0が含まれていることが書かれています。
20 emoji characters, including hair pick, maracas, jellyfish, khanda, and pink heart. For complete statistics regarding all emoji as of Unicode 15.0, see Emoji Counts. For more information about emoji additions in version 15.0, including new emoji ZWJ sequences and emoji modifier sequences, see Emoji Recently Added, v15.0.
Unicodeの中に、絵文字がバージョン入りで含まれているようです。
Unicode 13.0を見てみましょう。
こちらでは、Emoji 13.0での追加絵文字が書かれています。
55 emoji characters. For complete statistics regarding all emoji as of Unicode 13.0, see Emoji Counts. For more information about emoji additions in version 13.0, including new emoji ZWJ sequences and emoji modifier sequences, see Emoji Recently Added, v13.0.
絵文字
現時点での絵文字に関する情報、そして一覧はこちら。
Emoji 13.0の場合。
ですが、ちゃんとしたリストを見ようと思うと、こちらを参照した方が良さそうです。
たとえば、Emoji 13.0の場合。
https://unicode.org/Public/emoji/13.0/emoji-sequences.txt
https://unicode.org/Public/emoji/13.0/emoji-zwj-sequences.txt
絵文字と構造
絵文字とその構造は、以下のドキュメントに記載されています。
絵文字は、シーケンスというもので分類されるようです。
Unicode Emoji / Introduction / Definitions / Emoji Sequences
シーケンスには以下があります。
- emoji core sequence
- emoji character
- emoji keycap sequence
- emoji modifier sequence
- emoji flag sequence
- emoji zwj sequence
- emoji tag sequence
先ほど、emoji-sequences.txt
とemoji-zwj-sequences.txt
の2つのファイルを紹介しましたが、どのファイルにどのシーケンスが
含まれているかは、以下に記載があります。
Unicode Emoji / Introduction / Definitions / Emoji Sets
また以下は最新のEmojiバージョンで追加された絵文字ですが、どのシーケンスに分類されるかが記載されています。
複数の絵文字を組み合わせる話は、以下あたりに記載があります。
- Unicode Emoji / Design Guidelines / Diversity
- Unicode Emoji / Design Guidelines / Multi-Person Groupings
- Unicode Emoji / Design Guidelines / Emoji ZWJ Sequences
- Unicode Emoji / Design Guidelines / Color
- Unicode Emoji / Design Guidelines / Emoji Glyph Facing Direction
- Unicode Emoji / Annex C: Valid Emoji Tag Sequences
ZWJというのは、ゼロ幅接合子(Zero Width Jointer)のことで、コードポイントではU+200D
を指します。
こちらを使って、複数の絵文字を接合して表示する仕組みになっています。
今回の絵文字では、 👨👩👧👦 (👨‍👩‍👧‍👦
)で使われています。
大人2人と子ども2人の絵文字を使って、家族の絵文字を形成しています。
Unicode拡張書記素クラスタ境界
前置きが長くなりましたが、BreakIterator
でうまく扱えない文字で紹介した以下のような絵文字を分割するには
正規表現「Unicode拡張書記素クラスタ境界(\b{g}
)」を使います。
絵文字 | 数値文字参照 |
---|---|
👆🏼 | 👆🏼 |
🇦🇿 | 🇦🇿 |
🏴 | 🏴󠁧󠁢󠁥󠁮󠁧󠁿 |
👨👩👧👦 | 👨‍👩‍👧‍👦 |
なお、Java 13以降を使う必要があります。
Pattern
クラスのJavadocに書かれていますね。
「Unicode拡張書記素クラスタ」は、書記素クラスタ・マッチャ\Xと対応する境界マッチャ\b{g}によってサポートされています。
なお、Unicode拡張書記素クラスタ自体はJava 9で導入されていました。
[JDK-7071819] To support Extended Grapheme Clusters in Regex - Java Bug System
Java 9のPattern
クラスの時点で、すでに記載があります。
Java 8の時点ではありませんでした。
Unicode拡張書記素クラスタについては、以下に記載があります。
ひとつまたはそれ以上のUnicode文字が、人が1文字として捉えるものを構成することがあります。文字というものに対して、コンピューター上での
曖昧さを避けるためのもので、これを「書記素クラスタ」と呼びます。
One or more Unicode characters may make up what the user thinks of as a character. To avoid ambiguity with the computer use of the term character, this is called a grapheme cluster.
上記ドキュメントは、正規表現エンジンをUnicodeに適合させるためのガイドラインです。
いきなり「拡張(Extended)」とあるので、そうではないものがあるのでは?と思ったのですが、以前の書記素クラスタは「Legacy」と
呼ばれ、下位互換性のために残されているようです。
The Unicode Standard provides default algorithms for determining grapheme cluster boundaries, with two variants: legacy grapheme clusters and extended grapheme clusters. The most appropriate variant depends on the language and operation involved. However, the extended grapheme cluster boundaries are recommended for general processing, while the legacy grapheme cluster boundaries are maintained primarily for backwards compatibility with earlier versions of this specification.
Unicode Text Segmentation / Grapheme Cluster Boundaries
上記のドキュメントは、Unicodeテキストのセグメンテーションを決めるためのガイドラインです。その中に書記素クラスタについて
触れられている箇所があります。
サポートするUnicode拡張書記素クラスタのアップグレード
今回挙げた絵文字を扱うためにはJava 13以降を使う必要がある、と書いたのは以下のリリースノートに関連します。
Java 13でUnicode 12.1をサポートしましたが、
Bug ID: JDK-8221431 Support for Unicode 12.1
この時にUnicode拡張書記素クラスタのアップグレードを行っています。
一連の修正コミット。
8221431: Support for Unicode 12.1 · openjdk/jdk17u@93fabcd · GitHub
この対応がポイントです。
その関連issueとして、Unicode拡張書記素クラスタ境界で複数の絵文字から成る「フラグ(旗)」の絵文字が、うまく扱えなかったという
事象が報告されていました。
[JDK-8209777] \b{g} in regexes fails to break between flag emoji - Java Bug System
今回は、Java 17でUnicode拡張書記素クラスタ境界を使って文字を分割してみたいと思います。
なお、動作確認時点でのPattern
とGrapheme
(Unicode拡張書記素クラスタに関するクラス)のソースコードは、こちら。
環境
今回の環境は、こちらです。
$ java --version openjdk 17.0.4 2022-07-19 OpenJDK Runtime Environment (build 17.0.4+8-Ubuntu-120.04) OpenJDK 64-Bit Server VM (build 17.0.4+8-Ubuntu-120.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 17.0.4, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-126-generic", arch: "amd64", family: "unix"
準備
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.23.1</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> </plugins> </build>
確認は、前エントリーと同様にテストコードで行うことにします。
テストコードの雛形。
src/test/java/CountCharTest.java
import java.text.BreakIterator; import java.util.function.Function; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class CountCharTest { // ここに、テストを書く }
お題
今回は、BMPの範囲の文字、サロゲートペア、異体字セレクタを使った文字、絵文字、複数の絵文字からなる絵文字をそれぞれ数えてみます。
対象は以下にしました。前回のエントリーでの確認内容も入れています。
@Test public void print() { String bmpString = "羽"; String surrogatePairString = "\uD867\uDE3D"; // U+29E3D String variationSelectorString = "飴\uDB40\uDD01"; // U+98F4 U+E0101 String emojiString = "🍀"; // U+1F340 String combinedEmojiString1 = "\uD83D\uDC46\uD83C\uDFFC"; // U+1F446 U+1F3FC String combinedEmojiString2 = "\uD83C\uDDE6\uD83C\uDDFF"; // U+1F1E6 U+1F1FF String combinedEmojiString3 = "\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F"; // U+1F3F4 U+E0067 U+E0062 U+E0065 U+E006E U+E0067 U+E007F String combinedEmojiString4 = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"; // U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466 System.out.printf("BMP example: %s%n", bmpString); System.out.printf("SurrogatePair example: %s%n", surrogatePairString); System.out.printf("VariationSelector example: %s%n", variationSelectorString); System.out.printf("Emoji example: %s%n", emojiString); System.out.printf("Combined Emoji2 example: %s%n", combinedEmojiString1); System.out.printf("Combined Emoji2 example: %s%n", combinedEmojiString2); System.out.printf("Combined Emoji3 example: %s%n", combinedEmojiString3); System.out.printf("Combined Emoji4 example: %s%n", combinedEmojiString4); }
標準出力で確認するとこのようになりました。
※この箇所だけ、家族の絵文字がうまく貼れませんでした…
BMP example: 羽 SurrogatePair example: 𩸽 VariationSelector example: 飴󠄁 Emoji example: 🍀 Combined Emoji2 example: 👆🏼 Combined Emoji2 example: 🇦🇿 Combined Emoji3 example: 🏴 Combined Emoji4 example: 👨<200d>👩<200d>👧<200d>👦
Unicodeコードポイントで見ると、このようになります。
@Test public void unicodeCodePoints() { String bmpString = "羽"; String surrogatePairString = "\uD867\uDE3D"; // U+29E3D String variationSelectorString = "飴\uDB40\uDD01"; // U+98F4 U+E0101 String emojiString = "🍀"; // U+1F340 String combinedEmojiString1 = "\uD83D\uDC46\uD83C\uDFFC"; // U+1F446 U+1F3FC String combinedEmojiString2 = "\uD83C\uDDE6\uD83C\uDDFF"; // U+1F1E6 U+1F1FF String combinedEmojiString3 = "\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F"; // U+1F3F4 U+E0067 U+E0062 U+E0065 U+E006E U+E0067 U+E007F String combinedEmojiString4 = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"; // U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466 assertThat(bmpString.codePoints().mapToObj(v -> "0x" + Integer.toHexString(v)).collect(Collectors.toList())) .containsExactly("0x7fbd"); assertThat(surrogatePairString.codePoints().mapToObj(v -> "0x" + Integer.toHexString(v)).collect(Collectors.toList())) .containsExactly("0x29e3d"); assertThat(variationSelectorString.codePoints().mapToObj(v -> "0x" + Integer.toHexString(v)).collect(Collectors.toList())) .containsExactly("0x98f4", "0xe0101"); assertThat(emojiString.codePoints().mapToObj(v -> "0x" + Integer.toHexString(v)).collect(Collectors.toList())) .containsExactly("0x1f340"); assertThat(combinedEmojiString1.codePoints().mapToObj(v -> "0x" + Integer.toHexString(v)).collect(Collectors.toList())) .containsExactly("0x1f446", "0x1f3fc"); assertThat(combinedEmojiString2.codePoints().mapToObj(v -> "0x" + Integer.toHexString(v)).collect(Collectors.toList())) .containsExactly("0x1f1e6", "0x1f1ff"); assertThat(combinedEmojiString3.codePoints().mapToObj(v -> "0x" + Integer.toHexString(v)).collect(Collectors.toList())) .containsExactly("0x1f3f4", "0xe0067", "0xe0062", "0xe0065", "0xe006e", "0xe0067", "0xe007f"); assertThat(combinedEmojiString4.codePoints().mapToObj(v -> "0x" + Integer.toHexString(v)).collect(Collectors.toList())) .containsExactly("0x1f468", "0x200d", "0x1f469", "0x200d", "0x1f467", "0x200d", "0x1f466"); }
Unicode拡張書記素クラスタ境界を使う
では、Unicode拡張書記素クラスタ境界(\b{g}
)を使ってみます。
@Test public void extendedGraphemeClusterRegex() { String bmpString = "羽"; String surrogatePairString = "\uD867\uDE3D"; // U+29E3D String variationSelectorString = "飴\uDB40\uDD01"; // U+98F4 U+E0101 String emojiString = "🍀"; // U+1F340 String combinedEmojiString1 = "\uD83D\uDC46\uD83C\uDFFC"; // U+1F446 U+1F3FC String combinedEmojiString2 = "\uD83C\uDDE6\uD83C\uDDFF"; // U+1F1E6 U+1F1FF String combinedEmojiString3 = "\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F"; // U+1F3F4 U+E0067 U+E0062 U+E0065 U+E006E U+E0067 U+E007F String combinedEmojiString4 = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"; // U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466 assertThat(bmpString.split("\\b{g}").length).isEqualTo(1); assertThat(surrogatePairString.split("\\b{g}").length).isEqualTo(1); assertThat(variationSelectorString.split("\\b{g}").length).isEqualTo(1); assertThat(emojiString.split("\\b{g}").length).isEqualTo(1); assertThat(combinedEmojiString1.split("\\b{g}").length).isEqualTo(1); assertThat(combinedEmojiString2.split("\\b{g}").length).isEqualTo(1); assertThat(combinedEmojiString3.split("\\b{g}").length).isEqualTo(1); assertThat(combinedEmojiString4.split("\\b{g}").length).isEqualTo(1); }
すべて1文字になりました。
BreakIterator
BreakIterator
でも確認してみましょう。
@Test public void breakIterator() { String bmpString = "羽"; String surrogatePairString = "\uD867\uDE3D"; // U+29E3D String variationSelectorString = "飴\uDB40\uDD01"; // U+98F4 U+E0101 String emojiString = "🍀"; // U+1F340 String combinedEmojiString1 = "\uD83D\uDC46\uD83C\uDFFC"; // U+1F446 U+1F3FC String combinedEmojiString2 = "\uD83C\uDDE6\uD83C\uDDFF"; // U+1F1E6 U+1F1FF String combinedEmojiString3 = "\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F"; // U+1F3F4 U+E0067 U+E0062 U+E0065 U+E006E U+E0067 U+E007F String combinedEmojiString4 = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"; // U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466 Function<String, Integer> counter = w -> { BreakIterator iterator = BreakIterator.getCharacterInstance(); iterator.setText(w); int count = 0; while (iterator.next() != BreakIterator.DONE) { count++; } return count; }; assertThat(counter.apply(bmpString)).isEqualTo(1); assertThat(counter.apply(surrogatePairString)).isEqualTo(1); assertThat(counter.apply(variationSelectorString)).isEqualTo(1); assertThat(counter.apply(emojiString)).isEqualTo(1); assertThat(counter.apply(combinedEmojiString1)).isEqualTo(2); assertThat(counter.apply(combinedEmojiString2)).isEqualTo(2); assertThat(counter.apply(combinedEmojiString3)).isEqualTo(7); assertThat(counter.apply(combinedEmojiString4)).isEqualTo(7); }
こちらは、複数の絵文字から構成される絵文字が軒並みうまく扱えていません。
というわけで、Java 13以降であればUnicode拡張書記素クラスタ境界(\b{g}
)を使った方が良さそうです。
Java 11だとどうなるのか
ところで、Java 11だとUnicode拡張書記素クラスタ境界(\b{g}
)がどういう結果になるのか試してみることにしました。
素直にテストコードを書くと失敗したところで止まってしまうので、こんなコードを用意。
こちらは、文字数を表示します。
@Test public void printExtendedGraphemeClusterRegexLength() { String bmpString = "羽"; String surrogatePairString = "\uD867\uDE3D"; // U+29E3D String variationSelectorString = "飴\uDB40\uDD01"; // U+98F4 U+E0101 String emojiString = "🍀"; // U+1F340 String combinedEmojiString1 = "\uD83D\uDC46\uD83C\uDFFC"; // U+1F446 U+1F3FC String combinedEmojiString2 = "\uD83C\uDDE6\uD83C\uDDFF"; // U+1F1E6 U+1F1FF String combinedEmojiString3 = "\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F"; // U+1F3F4 U+E0067 U+E0062 U+E0065 U+E006E U+E0067 U+E007F String combinedEmojiString4 = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"; // U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466 System.out.printf("%s: splitted length = %d%n", bmpString, bmpString.split("\\b{g}").length); System.out.printf("%s: splitted length = %d%n", surrogatePairString, surrogatePairString.split("\\b{g}").length); System.out.printf("%s: splitted length = %d%n", variationSelectorString, variationSelectorString.split("\\b{g}").length); System.out.printf("%s: splitted length = %d%n", emojiString, emojiString.split("\\b{g}").length); System.out.printf("%s: splitted length = %d%n", combinedEmojiString1, combinedEmojiString1.split("\\b{g}").length); System.out.printf("%s: splitted length = %d%n", combinedEmojiString2, combinedEmojiString2.split("\\b{g}").length); System.out.printf("%s: splitted length = %d%n", combinedEmojiString3, combinedEmojiString3.split("\\b{g}").length); System.out.printf("%s: splitted length = %d%n", combinedEmojiString4, combinedEmojiString4.split("\\b{g}").length); }
こちらは、ここまでに登場した全文字をつなげて、どのように分割されるのかを表示します。
@Test public void splitCharsUsingExtendedGraphemeClusterRegex() { String string = "羽\uD867\uDE3D飴\uDB40\uDD01🍀\uD83D\uDC46\uD83C\uDFFC\uD83C\uDDE6\uD83C\uDDFF\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"; for (String c : string.split("\\b{g}")) { int start = string.indexOf(c); int end = start + c.length(); System.out.printf("extended grapheme cluster regex: char(%d, %d) = %s%n", start, end, c); } }
それぞれ、結果はこうなりました。
羽: splitted length = 1 𩸽: splitted length = 1 飴󠄁: splitted length = 1 🍀: splitted length = 1 👆🏼: splitted length = 1 🇦🇿: splitted length = 1 🏴: splitted length = 1 👨👩👧👦: splitted length = 1 extended grapheme cluster regex: char(0, 1) = 羽 extended grapheme cluster regex: char(1, 3) = 𩸽 extended grapheme cluster regex: char(3, 6) = 飴󠄁 extended grapheme cluster regex: char(6, 8) = 🍀 extended grapheme cluster regex: char(8, 12) = 👆🏼 extended grapheme cluster regex: char(12, 16) = 🇦🇿 extended grapheme cluster regex: char(16, 30) = 🏴 extended grapheme cluster regex: char(30, 41) = 👨👩👧👦
ここで、Java 11に切り替え、
$ java --version openjdk 11.0.16 2022-07-19 OpenJDK Runtime Environment (build 11.0.16+8-post-Ubuntu-0ubuntu120.04) OpenJDK 64-Bit Server VM (build 11.0.16+8-post-Ubuntu-0ubuntu120.04, mixed mode, sharing)
pom.xml
でのコンパイルオプションも変更して再度実行してみます。
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
すると、確かにおかしな結果になりました。
羽: splitted length = 1 𩸽: splitted length = 1 飴󠄁: splitted length = 1 🍀: splitted length = 1 👆🏼: splitted length = 2 🇦🇿: splitted length = 1 🏴: splitted length = 7 👨👩👧👦: splitted length = 4 extended grapheme cluster regex: char(0, 1) = 羽 extended grapheme cluster regex: char(1, 3) = 𩸽 extended grapheme cluster regex: char(3, 6) = 飴󠄁 extended grapheme cluster regex: char(6, 8) = 🍀 extended grapheme cluster regex: char(8, 10) = 👆 extended grapheme cluster regex: char(10, 12) = 🏼 extended grapheme cluster regex: char(12, 16) = 🇦🇿 extended grapheme cluster regex: char(16, 18) = 🏴 extended grapheme cluster regex: char(18, 20) = extended grapheme cluster regex: char(20, 22) = extended grapheme cluster regex: char(22, 24) = extended grapheme cluster regex: char(24, 26) = extended grapheme cluster regex: char(18, 20) = extended grapheme cluster regex: char(28, 30) = extended grapheme cluster regex: char(30, 33) = 👨 extended grapheme cluster regex: char(33, 36) = 👩 extended grapheme cluster regex: char(36, 39) = 👧 extended grapheme cluster regex: char(39, 41) = 👦
確かに、以前のバージョンではうまく機能しないようですね。
その他
ここから先は、Javaのバージョンで結果が変わらない方法で見ていきます。内容的には、以前のエントリーで書いたものと同じですが。
String#length
今回のお題だと、String#length
では文字数をうまく数えられないものばかりになります。
@Test public void stringLength() { String bmpString = "羽"; String surrogatePairString = "\uD867\uDE3D"; // U+29E3D String variationSelectorString = "飴\uDB40\uDD01"; // U+98F4 U+E0101 String emojiString = "🍀"; // U+1F340 String combinedEmojiString1 = "\uD83D\uDC46\uD83C\uDFFC"; // U+1F446 U+1F3FC String combinedEmojiString2 = "\uD83C\uDDE6\uD83C\uDDFF"; // U+1F1E6 U+1F1FF String combinedEmojiString3 = "\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F"; // U+1F3F4 U+E0067 U+E0062 U+E0065 U+E006E U+E0067 U+E007F String combinedEmojiString4 = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"; // U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466 assertThat(bmpString.length()).isEqualTo(1); assertThat(surrogatePairString.length()).isEqualTo(2); assertThat(variationSelectorString.length()).isEqualTo(3); assertThat(emojiString.length()).isEqualTo(2); assertThat(combinedEmojiString1.length()).isEqualTo(4); assertThat(combinedEmojiString2.length()).isEqualTo(4); assertThat(combinedEmojiString3.length()).isEqualTo(14); assertThat(combinedEmojiString4.length()).isEqualTo(11); }
String#codePointCount
String#codePointCount
は異体字セレクタ以外は1になりました。絵文字もうまく扱えるんですね。
@Test public void codePointCount() { String bmpString = "羽"; String surrogatePairString = "\uD867\uDE3D"; // U+29E3D String variationSelectorString = "飴\uDB40\uDD01"; // U+98F4 U+E0101 String emojiString = "🍀"; // U+1F340 String combinedEmojiString1 = "\uD83D\uDC46\uD83C\uDFFC"; // U+1F446 U+1F3FC String combinedEmojiString2 = "\uD83C\uDDE6\uD83C\uDDFF"; // U+1F1E6 U+1F1FF String combinedEmojiString3 = "\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F"; // U+1F3F4 U+E0067 U+E0062 U+E0065 U+E006E U+E0067 U+E007F String combinedEmojiString4 = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"; // U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466 assertThat(bmpString.codePointCount(0, bmpString.length())).isEqualTo(1); assertThat(surrogatePairString.codePointCount(0, surrogatePairString.length())).isEqualTo(1); assertThat(variationSelectorString.codePointCount(0, variationSelectorString.length())).isEqualTo(2); assertThat(emojiString.codePointCount(0, emojiString.length())).isEqualTo(1); assertThat(combinedEmojiString1.codePointCount(0, emojiString.length())).isEqualTo(1); assertThat(combinedEmojiString2.codePointCount(0, emojiString.length())).isEqualTo(1); assertThat(combinedEmojiString3.codePointCount(0, emojiString.length())).isEqualTo(1); assertThat(combinedEmojiString4.codePointCount(0, emojiString.length())).isEqualTo(1); }
BreakIteratorで文字を分割
BreakIterator
を使って文字を数えるコードはすでに載せましたが、どんな感じの分割のされ方をしていたか確認してみましょう。
Unicode拡張書記素クラスタ境界(\b{g}
)を使って、今回扱ったすべての文字をつなげて、分割するコードをBreakIterator
を使って
書いてみます。
@Test public void splitCharsUsingBreakIterator() { String string = "羽\uD867\uDE3D飴\uDB40\uDD01🍀\uD83D\uDC46\uD83C\uDFFC\uD83C\uDDE6\uD83C\uDDFF\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"; BreakIterator iterator = BreakIterator.getCharacterInstance(); iterator.setText(string); for (int start = iterator.first(), end = iterator.next(); end != BreakIterator.DONE; start = end, end = iterator.next()) { String c = string.substring(start, end); System.out.printf("break iterator: char(%d, %d) = %s%n", start, end, c); } }
結果。
break iterator: char(0, 1) = 羽 break iterator: char(1, 3) = 𩸽 break iterator: char(3, 6) = 飴󠄁 break iterator: char(6, 8) = 🍀 break iterator: char(8, 10) = 👆 break iterator: char(10, 12) = 🏼 break iterator: char(12, 14) = 🇦 break iterator: char(14, 16) = 🇿 break iterator: char(16, 18) = 🏴 break iterator: char(18, 20) = break iterator: char(20, 22) = break iterator: char(22, 24) = break iterator: char(24, 26) = break iterator: char(26, 28) = break iterator: char(28, 30) = break iterator: char(30, 32) = 👨 break iterator: char(32, 33) = break iterator: char(33, 35) = 👩 break iterator: char(35, 36) = break iterator: char(36, 38) = 👧 break iterator: char(38, 39) = break iterator: char(39, 41) = 👦
やっぱり、各絵文字がバラバラに切り出されてしまいました。
代替手段
Java 13以前の環境で、Unicode拡張書記素クラスタ境界(\b{g}
)と同じように文字を数えたい(分割したい)場合は、ICU4JのBreakIterator
を
使えばよいみたいです。
まとめ
これまで先送りにしていた、Unicode拡張書記素クラスタ境界(\b{g}
)を使った文字の数え方(分割の仕方)を試してみました。
使い方そのものよりも、絵文字に関する情報を調べる方が大変でしたね…。