LoginSignup
3
2

More than 5 years have passed since last update.

Scalaでサロゲートペア文字を除去する…から脱線してscala-java8-compatを使う話

Last updated at Posted at 2015-11-11

[scala | java] サロゲートペア文字列を排除する
こちらの記事を参考にしつつ、vardo~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.IntPredicatescala.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
  }
3
2
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2