来世から頑張る!!

技術ブログを目指して

Scalaでの🍣の数え方

最近のおっさんはビールもきちんと数えられないことに驚愕したので、ScalaですがJavaの話をします。 昔と違ってUTF-8の半角カナが3バイトだと信じてくれないおじさんとは出会わなくなってきたなと思ってたのに、世の中そう甘くはなかったようです。

Javaの話なのでClojureだろうがKotlinだろうがJasminだろうが基本的におんなじです。

文字の数え方

🍺🍺🍺 <- これ、いくつかと聞かれたら3と答えて欲しいわけですよ!

scala> val beers = "🍺🍺🍺"
beers: String = 🍺🍺🍺

scala> print(beers.length)
6

当然、emojiなのでサロゲートペアですよ。 codePointCountを使ってくださいねという話です。

scala> print(beers.codePointCount(0, beers.length))
3

せっかくなのでScalaらしくpimpしちゃいましょう。 メソッド名はStringOpsとかとかぶらないように・・・

implicit class StringCount(val s: String) extends AnyVal {
  def letterCount: Int = s.codePointCount(0, s.length)
}

これでbeers.letterCountで文字数が出せるわけですね!

合成文字にも対応しておく

せっかくなので合成文字の数え方にも対応しておきましょう。
参考: Java SE 6 バラバラにして組み立てて - Normalizer

Java(というかUnicode)の正規化には2種類(2段階)あって、Å("\u0041\u030a")のような合成文字をÅ("\u00c5")に圧縮するだけの正規化Canonical Composeと、同じ意味を表すもっと(だいたい英語圏民に)一般的そうな文字に置き換えるCompatibility Composeがあります。

前者に使うのがjava.text.Normalizer.Form.NFC、後者に使うのがjava.text.Normalizer.Form.NFKCです。

試してみましょう。

scala> val a = "\u0041\u030a"
a: String = Å

scala> val b = "\u00c5"
b: String = Å

scala> a == b
res0: Boolean = false

scala> val c = Normalizer.normalize(a, Normalizer.Form.NFC)
c: String = Å

scala> b == c
res1: Boolean = true

scala> val d = "㌔㍉"
d: String = ㌔㍉

scala> val e = Normalizer.normalize(d, Normalizer.Form.NFC)
e: String = ㌔㍉

scala> val f = Normalizer.normalize(d, Normalizer.Form.NFKC)
f: String = キロミリ

文字数を数えたいので、使うのはNFCですね。

import java.text.Normalizer
implicit class StringCount(val s: String) extends AnyVal {
  def normalized: String = Normalizer.normalize(s, Normalizer.Form.NFC)
  def letterCount: Int = {
    val n = this.normlized
    n.codePointCount(0, n.length)
  }
}
scala> val x = "🍺\u0041\u030a🍣\u0041\u030a🍺"
x: String = 🍺Å🍣Å🍺

scala> x.length
res0: Int = 10

scala> x.letterCount
res1: Int = 5

寿司が2貫で1個なのかはともかく、酔っぱらっても1つのビールが2つに見えるようにはなりたくないものです。