Ruby で絵文字を含む文字列の長さをカウントしたい

1
2
3
4
str = ""
=> ""
str.size
=> 3

なんでやねん!?

関西人の廣田です。
ちょこファことちょこっとファームでサーバーサイドエンジニアをやっています。

先日、ちょこファの機能追加で、絵文字を含む文字列の文字列長をカウントする必要が出てきました。
しかし普通にカウントすると上のように残念な結果になります。

Objective-Cで絵文字をいい感じにカウントするには、Qiitaの記事がとても参考になると思います。

Ojbective-Cで絵文字を見た目通りにカウントする
http://qiita.com/matsuokah/items/a435e3c86318a793d307

本記事では、先のリンク先を参考にRubyで絵文字の長さを正しくカウントしていきたいと思います。

絵文字に使われる特殊文字たち

絵文字には以下の5種類の特殊文字が使われています。

これらが入ることで余計な文字がカウントされてしまいます。
基本的にはそれぞれのUnicodeを正規表現で拾ってきてよしなにカウントすればOKです。
1つずつ、具体的に見てみましょう。

Regional Character

国旗の絵文字を定義するために使われる文字です。
Unicode0xdde6から0xddffで定義されています。
これらはいわば特殊なアルファベットで、例えば??なら0xddef(特殊な“J”)と0xddf5(特殊な“P”)の2文字で定義されます。
そのため、すべての国旗の絵文字は文字列長2でカウントされます。
これを回避するには、Regional Characterが2文字続けて出現した時は適当な1文字に置き換えてやればOKです。

str = "??????"
str.size
=> 6
str2 = str.gsub(/[\u{1F1E6}-\u{1F1FF}]{2}/, "0")
str2.size
=> 3

いい感じですね???

Skin tone modifier

顔や人の絵文字の肌の色を変えるための文字です。
通常の顔の絵文字にはつかないのですが、肌の色が違う場合は通常の文字の後ろにこのSkin tone modifierがついて、合わせて2文字でカウントされます。
0xdffbから0xdfffで5種類の肌の色を表現しているので、これらを消してしまえばOKです。

str = "???"
str.size
=> 3
str2 = str.gsub(/[\u{1F3FB}-\u{1F3FF}]/, "")
=> "??"
str2.size
=> 2

にっこりです???

Variation selector

同じ記号でも違う表示をしたい時に使われる文字です。
例えば❤︎とです。
この2文字、本体のハートの部分は0x2764で定義されているのですが、その後ろに0xfe0fがつくとに表示が変わります。
Valiation selector0xfe00から0xfe0fの16種が定義されているので、あとはSkin tone modifierと一緒ですね。

Zero-width Joiner

iOSやOS Xで家族の絵文字を定義するのに使われます。
実は家族の絵文字の中身は家族の人数分の人の絵文字で、それらの間にこのZero-width Joinerが挟まれると家族の絵文字と認識されます。
つまり、4人家族の絵文字なら7文字分カウントされるというわけですね。
あと、家族の絵文字はApple製品でしか定義されていないというのが肝で、Androidなどから見ると家族の人数分の人の絵文字が普通に表示されるだけになってしまいます。
そのため、今回は家族の絵文字は1文字ではなく、その家族の人数分の文字列長として扱うことにします。
Zero-width Joiner0x200dで定義されているので、これを消してやればOKです。

str = "?‍?‍?‍?"
str.size
=> 7
str2 = str.delete("\u{200D}")
=> "????"
str2.size
=> 4

おけまる???

KeyCap

長くなりましたがラストです。
最後はKeyCap、囲み文字です。
0x20e3で定義されており、数字の絵文字を表現するのに使われています。
数字の絵文字は普通の半角数字の後ろにこのKeyCapをつけて表現しています。
また、数字の絵文字にはもう一点注意があり、Apple製品では半角数字 + 0xfe0f(Valiation Selector) + KeyCapで表現されます。
そのため、AndroidとApple製品、両方の数字の絵文字を正しくカウントするには以下のようにするのがいいでしょう。

1
2
3
4
5
6
str = ""
str.size
=> 9
str2 = str.gsub(/[0-9]\u{FE0F}?\u{20E3}/, "0")
str2.size
=> 3

お疲れ様でした???

まとめ

今回の機能追加で、絵文字とついでに正規表現と仲良くなった気がします。
絵文字はなかなか闇の深い規格だと思いましたが、これでしばらくは楽しい絵文字コミュニケーションライフが送れそうです。

また、本記事の作成にあたって、先輩エンジニアの方々が絵文字入力対応を行ってくださいました。
ありがとうございます?

参考にしたサイト