RM-BLOG

IT系技術職のおっさんがIT技術とかライブとか日常とか雑多に語るブログです。* 本ブログに書かれている内容は個人の意見・感想であり、特定の組織に属するものではありません。/All opinions are my own.*

【java】サロゲートペア置換

いろいろ探したのだが、個人的に求めている「コレ!」というコーディングサンプルが見つからなかったので
自分なりに作ってみた「サロゲートペア置換」のjava実装。
(もっとスマートなやり方はないものだろうか)




やりたかったのは「サロゲートペアを元の文字数を維持しつつ置換する」である。
たとえば「あ𩸽」というStringがあった場合、
String#length()は見た目に反して2ではなく3になってしまう。
このため1文字ずつ文字を取得していく(String#lengthをループ上限にしてString#charAtで1文字ずつ取得する)と
2文字なのに3回ループすることになり、
サロゲートだったら」が後半2回のループにあたって3文字になってしまう。
これをなんとかうまい具合に処理する方法はないものかと
いろいろネット探したのだが見当たらない。
仕方ないので自分で実装した。(ただいろいろ甘い気はする)

あ:0x3042
𩸽:0xD867,0xDE3D(0x29E3D)
ということだが、
↑の方法でループさせると
 0x3042(1回目)⇒0xD867(2回目)⇒0xDE3D(3回目)
になる。
後半2回のループは実質2つのコードで1文字を表してるから
1回目のループ分1文字と合わせて
3回ループしたとしても「2文字」にしたい。
…というのが”やりたいこと”だった。

で、実際に書いたコードがこれ↓

	private static String convertSurrogatePair(String plainText) {
		char[] plainChars = plainText.toCharArray();
		
		StringBuilder convChars = new StringBuilder();
		for (int i=0; i < plainChars.length; i++){
			char c = plainChars[i];
			String ret = "";
			if (Character.isLowSurrogate(c) || Character.isHighSurrogate(c)) {
				ret = "Surrogate";
				// 最初の要素以外は前要素のコードポイントを見て2コードでサロゲートになるかチェック
				if (i > 0){
					
					char bef_c = (char)Character.codePointBefore(plainChars,i);
					if (Character.isSurrogatePair(bef_c,c)) {
						ret = "Surrogate Pair With " + Integer.toHexString(bef_c) + "[" + String.valueOf(i-1) + "]";
						convChars.append(CONV_SURROGATE_STR);
					}
				}
				
			} else {
				// サロゲートじゃなければ元の文字をそのまま生かす
				ret = "No Surrogate";
				
				convChars.append(c);
			}
			
			String hexStr = Integer.toHexString(c);
			
			System.out.println(String.valueOf(c) + " - " + hexStr + " - " + ret);
		}
		String convText = convChars.toString();
		System.out.println("Original String:" + plainText);
		System.out.println("Convert  String:" + convText);
		System.out.println();
		
		return convText;
	}



変換前の文字列[A]の中のサロゲートペアを"?"(全角ハテナ)に変換して
変換後の文字列[B]に格納する。

 (0)以下(1)~(3)を[A]の全文字に対して1文字ずつ処理する
 (1)[A]から1文字を取得する
 (2)(1)のコードポイントを取得する
 (3)(2)が下位サロゲートもしくは上位サロゲートに該当するかチェックする
   (3-1)下位サロゲートもしくは上位サロゲートの場合
     (3-1-1)検査中の要素が0番目以降の場合
        (3-1-1-1)前要素のコードポイントを取得し
                 自要素のコードポイントと合わせてサロゲートペアかどうかチェックする
           (3-1-1-1-1)サロゲートペアの場合、"?"を[B]に格納する
   (3-2)下位サロゲートでも上位サロゲートでもない場合
       (1)を[B]に格納する

↑プログラムを正当に翻訳するとこんな感じになるのか。
ifのネストが深いな。
もうちょいやりかたありそうなもんだが。



これをやると、「あ𩸽」は「あ?」になるので(喧嘩売ってるみたいだね)
やろうと思っていた「見た目の文字数を維持」については満たすことができる。
しかし一方でバイト数が変換前後で変わってしまう。
変換前は「あ𩸽」で7バイト(あで3バイト、𩸽で4バイト)だが
変換後は「あ?」で6バイト(あで3バイト、?で3バイト)なので
変換後のほうがバイト数が少なくなる。
なのでバイト数でチェックするような仕組みがある場合は変換前後で動作が変わるので、
使用用途によっては使えないだろう。
少なくなる分にはDBエラーとか起きなそうだけど。

バイト数を合わせる場合は、例えば3バイト文字+1バイト文字にすればいいので、
例えば「あ𩸽」⇒「あ??」とかにすれば(やっぱり喧嘩売ってるみたいだね)いいのだが、
そうすると変換前後でバイト数は合うものの文字数が合わなくなるので
やっぱりこれも使用用途に寄る。
ただまあ「文字数」と聞いてjava的に最も身近に実装したくなるString#lengthでいえば
このやり方は変換前後で値が一致するので、
サロゲートを除去」という目的に沿うならこっちのほうがなんとなく正しそうな気もする。