[scala | java] サロゲートペア文字列を排除する
こちらの記事を参考にしつつ、var
やdo~while
を使わず書きたい、というのが出発点。
java.text.BreakIterator を使う(1)
def removeSurrogate(s: String): String = {
val itr = BreakIterator.getCharacterInstance
itr.setText(s)
val last = itr.last()
Iterator.iterate((itr.first(), Option.empty[Char])) { case (index, _) =>
val c = PartialFunction.condOpt(index) {
case i if i < last && s.codePointAt(i) <= 0xFFFF => s.charAt(i)
}
(itr.next(), c)
}.takeWhile { case (i, _) => i != BreakIterator.DONE}.toList.unzip._2.flatten.mkString
}
元記事を少し書き直した版。
java.text.BreakIterator を使う(2)
def removeSurrogate(s: String): String = {
val itr = BreakIterator.getCharacterInstance
itr.setText(s)
val last = itr.last()
Iterator.iterate(itr.first())(_ => itr.next())
.takeWhile(i => i < last && i != BreakIterator.DONE)
.collect { case i if s.codePointAt(i) <= 0xFFFF => s.charAt(i)}
.toList.mkString
}
(1)と大差ない気もするけど…。
余談だけど、itr.next()
は直前に itr.last()
を呼んでいると BreakIterator.DONE
が返るので少しハマッた…。
Javadocに first()
, last()
, next()
でそれぞれイテレータの現在位置を設定すると書かれているのでドキュメントに目を通せば済む話ではあるのだけど…。
val first = itr.first() // 最初の位置を取ろう
val last = itr.last() // 最後の位置を取ろう
val n = itr.next() // 順番に境界を…と思ったら -1 !?
CharSequence.codePoints を使う
s.codePointAt(i)
を繰り返し処理で取らなくても、便利なのがJava8から入ってる!
という事でこれを使う。
返り値は IntStream
になるので、Java Stream API の filter
などが使えるようだ。
def removeSurrogate(s: String): String = {
s.codePoints.toArray.collect { case i if i <= 0xFFFF => i.toChar }.mkString
}
こんな感じで一旦配列にしてScalaの collect
を呼んでもいいけど、せっかくなので Stream API を上手い具合にScalaで書けないものか。
CharSequence.codePoints を使う (Java Stream API with scala-java8-compat)
scala.collection.JavaConversions._
でのJavaとScalaのコレクション変換のようなノリで、
java.util.function.IntPredicate
と scala.Function1[scala.Int , scala.Boolean]
などを変換できないかな、と調べてみたところ、
scala-java8-compatの存在を知った。
import scala.compat.java8.FunctionConverters._
def removeSurrogate(s: String): String = {
val f: Int => Char = _.toChar
val p: Int => Boolean = _ <= 0xFFFF
s.codePoints.filter(p.asJava).mapToObj(f.asJava).toArray.mkString
}
おお、やりたかったイメージに近い。
一旦変数に置いて.asJava
を呼ばないといけないのがイマイチな気もするが…
サロゲートペア文字を除去する関数もそれなりにシンプルに書け、
scala-java8-compat も試せて良かった。
(とはいえ、比べてみると先にtoArray
してcollect
を呼ぶひとつ前の書き方のほうがシンプルなようにも思えるな…)
追記: Character.isSurrogate を使う
コメントで教えてもらったけど、ややこしい事やらずにこれが一番シンプルかな。
def removeSurrogate(s: String): String = {
s.toCharArray.filterNot(_.isSurrogate).mkString
}