問題の由来
Java言語は、内部の文字コードに16ビット固定長のUnicodeを採用していたが、将来収録予定の文字に対して、16ビット固定長で表現できる 65536個の領域では不足してしまいました。その問題を解決するために、サロゲートペア(Surrogate Pair)が導入されました。サロゲートペアとは、16ビット表現できる領域に、ハイサロゲート(High Surrogate)を1024個、ローサロゲート(Low Surrogate)を1024個定義し、両者を組み合わせることで、約100万個(=1024x1024個)のコードポイントを表現する方式です。それ以外の領域には変更はなく、従来通り 16ビットで表現されるので、1個のコードポイントを表現するために、16ビットと 32ビットの表現形式が混在することになってしまいました。
String#length()
、String#subString()
で文字を操作する際に、サロゲートペアの1文字に対して1つのchar値が対応するわけではなく、2つのchar値が対応する形になっているため、正しい文字数やsubStringを取得できなくなります。
例:
// 検証対象文字:「𪛉𠀑𠀃」がサロゲートペア文字
String target = "𪛉𠀑𠀃の文字";
// 文字数取得
int len = target.length(); // 結果:9文字
// 最初から4文字の取得
String subTarget = target.substring(0, 4) // 結果:𪛉𠀑
解決方法1
java.text.BreakIterator
クラスを利用します。BreakIterator クラスは、人が認識するテキスト内の境界位置を見つけるメソッドを実装しています。
下記は、BreakIteratorを利用する場合にsubStringの取得例です。
/**
* サロゲートペアのsubString文字の取得
*
* @param target 対象文字列
* @param startIndex 開始位置(0から)
* @param endIndex 終了位置
* @return
*/
public String subString(String target, int startIndex, int endIndex) {
// 入力値の妥当性チェック省略
BreakIterator bi = BreakIterator.getCharacterInstance();
bi.setText(target);
StringBuffer sb = new StringBuffer();
// 繰り返し用開始位置
int start = bi.first();
// 繰り返し用終了位置
int end = 0;
// 文字数
int count = 0;
// 文字の最後まで繰り返し
while (bi.next() != BreakIterator.DONE) {
end = bi.current();
// 文字数カウントアップ
count++;
// 引数の開始位置と終了位置の間に文字を取得する
if (count >= (startIndex + 1) && count<= endIndex) {
sb.append(target.substring(start, end));
}
start = end;
}
return sb.toString();
}
解決方法2
コードポイントを利用する方法です。コードポイントとは、Unicodeで1文字分のコード(int型)を示しています。
例:サロゲート文字「𪛉」のコードポイントは173769
非サロゲート文字「の」のコードポイントは12398
コードポイントにを表現するには必要なchar値の数をわかれば、正しい文字数やsubStringを取得できます。
以下のAPIを利用し、subStringを取得します。
・Character#codePointAt(char[] a, int index)
:char 配列の指定されたインデックスにあるコードポイントを返します。
・Character#charCount(int codePoint)
:コードポイントを表すのに必要な char 値の数を判定します。サロゲートペアの場合は、2 を返し、そうでない場合は、1 を返します。
・Character#toChars(int codePoint)
:コードポイントを UTF-16 表現に変換します。
下記は、コードポイントを利用する場合にsubStringの取得例です。
/**
* サロゲートペアのsubString文字の取得
*
* @param target 対象文字列
* @param startIndex 開始位置(0から)
* @param endIndex 終了位置
* @return
*/
public String subString(String target, int startIndex, int endIndex) {
// 入力値の妥当性チェック省略
// char配列の取得
char[] charArray = target.toCharArray();
// コードポイント分だけの開始位置インデックスの取得(繰り返し用)
int offsetStart = target.offsetByCodePoints(0, startIndex);
// コードポイント分だけの終了位置インデックスの取得(繰り返し用)
int offsetEnd = target.offsetByCodePoints(0, endIndex);
// コードポイント初期値
int codePoint = 0;
StringBuffer sb = new StringBuffer();
for (int i = offsetStart; i < offsetEnd; i += Character.charCount(codePoint)) {
// カレント文字のコードポイントの取得
codePoint = Character.codePointAt(charArray, i);
sb.append(String.valueOf(Character.toChars(codePoint)));
}
return sb.toString();
}