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つに見えるようにはなりたくないものです。