JIS X 0213など、シフトJISやマイクロソフト コードページ932以外の文字をプログラム上で紙に印刷する場合には、入力された文字列を枠内に確実に収めるため、文字数を正しくカウントする必要があります。
JIS X 0213では複数のコードポイントで1文字を表す文字(合字)があります。このような場合、見た目の「1文字」を「grapheme」(書記素)と呼びます。印刷においては、書記素ひとつを「1文字」としてカウントする必要があるわけです。
さて、以下のサイト「文字数をカウントする7つの方法」では、java.text.BreakIteratorを使って書記素をカウントする方法を示しています。どうやらLINEで使われているカウントの方法らしい。
engineering.linecorp.com
ところが調べてみると、そのまま使うにはコーナーケースをカバーしきれていないようです……
https://repl.it/@satob/BreakIteratorTest
import java.text.BreakIterator;
class Main {
public static void main(String[] args) {
System.out.println("\u3042");
System.out.println(getGraphemeLength("\u3042"));
System.out.println("\ud842\udfb7");
System.out.println(getGraphemeLength("\ud842\udfb7"));
System.out.println("\u4fae\ufe00");
System.out.println(getGraphemeLength("\u4fae\ufe00"));
System.out.println("\u02e5\u02e9");
System.out.println(getGraphemeLength("\u02e5\u02e9"));
System.out.println("\u30ab\u309a");
System.out.println(getGraphemeLength("\u30ab\u309a"));
System.out.println("\u309a\u30ab");
System.out.println(getGraphemeLength("\u309a\u30ab"));
System.out.println("\u307e\u309a");
System.out.println(getGraphemeLength("\u307e\u309a"));
System.out.println("\u309a");
System.out.println(getGraphemeLength("\u309a"));
System.out.println("\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC66");
System.out.println(getGraphemeLength("\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC66"));
}
public static int getGraphemeLength(String value) {
BreakIterator it = BreakIterator.getCharacterInstance();
it.setText(value);
int count = 0;
while (it.next() != BreakIterator.DONE) {
count++;
}
return count;
}
}
まとめると、JIS X 0213の範囲では以下のような挙動をするようです。2.がキツいな……
- サロゲートペア、IVSは正しく処理されている
- 1コードポイント目・2コードポイント目とも書記素として正当な組み合わせの合字が2書記素とカウントされてしまう
- JIS X 0213として不正な合字でも無理やりカウントしてしまう
- (おまけ)ZWJは1書記素としてカウントされてしまう
対策としては以下のような感じかと思います。
- 文字列長チェックの前に、JIS X 0213として不正なコードポイントの並びがないか確認する
- 1コードポイント目・2コードポイント目とも書記素として正当な組み合わせの合字が含まれている場合、文字列長から引き算する