ruby-jp slackで質問したところ、識者の皆さんから回答をいただけたので、まとめます。回答いただいた皆さん、ありがとうございました。
端的な回答1
unicode-emoji gemを使う。(筆者未検証)
端的な回答2
Unicode プロパティによる文字クラス指定を利用するのが便利、しかし文字コード沼は深い。筆者の今回の用途では以下の判定(Githubにも上げました)でほぼ大丈夫そう。
PATTERN = /[\p{Emoji}\p{Emoji_Component}&&[:^ascii:]]/
STRINGS_WITH_EMOJI = [
'🍣',
'0️⃣',
"👩👩👧👧", "☃️", "🇵","🏻", "😴", "▶️", "🛌🏽", "🇵🇹", "🏴", "2️⃣", "🤾🏽♀️",
].freeze
STRINGS_WITHOUT_EMOJI = [
'1aあア@',
'東京都江東区新木場2丁目2−10',
'黒木 慎介',
].freeze
result = true
STRINGS_WITH_EMOJI.each do |s|
unless s.match?(PATTERN)
puts s + 'は絵文字を含むと判定されませんでした'
result = false
end
end
STRINGS_WITHOUT_EMOJI.each do |s|
if s.match?(PATTERN)
puts s + 'は絵文字を含むと判定されてしまいました'
result = false
end
end
if result
puts '判定は成功しました'
end
ruby3.0.1(仕事で使っているバージョン)にて確認。
解説
正規表現の文字クラス
Rubyの正規表現では文字クラスを利用でき、これを活用することで、半角数字とかアルファベットの小文字とかの様々な判定ができます。
しかし、ドキュメントに記載されている中には、絵文字の文字クラスはありません。
Unicodeプロパティによる文字クラス指定
Unicodeの1つ1つの文字にはプロパティがあり、「これは数字であるか」「これは16進数で使うか」「これは大文字か」などの情報が管理されています。
これをRubyの正規表現からも利用することができます。Rubyの正規表現でサポートされているプロパティの一覧を見ると、この中にEmoji
があるのが確認できます。
このEmojiプロパティ(「これは絵文字であるか」ですね)を使って
'🍣'.match?(/\p{Emoji}/) # => true
'foo'.match?(/\p{Emoji}/) # => false
で概ねうまく判定できるのですが、1つ落とし穴があります。
Emojiプロパティの中には、1(半角英数)が含まれる
1.to_s.match?(/\p{Emoji}/) # => true
動作確認をしていて気がついたのですが、半角数字(と、#*
)はEmojiプロパティの中に含まれています。これは次に述べるUnicodeの仕様が関わっています。
Unicodeの組み合わせ文字
Unicodeには、既存の文字に続けて特殊な文字を書くことで、1つの別の文字になる組み合わせがあります。
その中の1つが、1
(U+0031)と⃣
(U+20E3)を組み合わせた1️⃣
です。
半角数字と#*
はこの組み合わせの一部になるため、Emojiプロパティに含まれているようです。
なので、Emojiプロパティに含まれているけど半角数字や#*
ではない文字を絵文字とみなすことができそうです。
'🍣'.match?(/[\p{Emoji}&&[^0-9#*]]/) # => true
'foo'.match?(/[\p{Emoji}&&[^0-9#*]]/) # => false
1.to_s.match?(/[\p{Emoji}&&[^0-9#*]]/) # => false
しかし、これもうまく行かないケースがあります。先述の組み合わせ文字です。
'1️⃣'.match?(/[\p{Emoji}&&[^0-9#*]]/) # => false
なお、他にもUnicodeには複雑な構成の絵文字が存在します(サンプルコードのSTRINGS_WITH_EMOJIにまとめて記載しています)が、これらについてはEmojiプロパティでmatchできるようでした。
Emoji_Componentプロパティ
1️⃣
は1
と⃣
に分けて判定されますが、⃣
がEmojiプロパティに含まれないため、先述の正規表現ではmatch出来ませんでした。
上記のUnicode公式サイトで各文字のプロパティが確認できます。(このサイトめっちゃ便利、教えてくださったima1zumiさん本当にありがとうございます)
Emoji_Componentプロパティには含まれているようです。組み合わせ文字用の文字はこのプロパティでカバーできそうです。
ついでに、0-9#*
を少し一般化して、ASCII文字とします。
'🍣'.match?(/[\p{Emoji}\p{Emoji_Component}&&[:^ascii:]]/) # => true
'foo'.match?(/[\p{Emoji}\p{Emoji_Component}&&[:^ascii:]]/) # => false
1.to_s.match?(/[\p{Emoji}\p{Emoji_Component}&&[:^ascii:]]/) # => false
'1️⃣'.match?(/[\p{Emoji}\p{Emoji_Component}&&[:^ascii:]]/) # => true
うまくいってそうです。
バイト長での判定はできるか?
バイト長が4だったら絵文字 という判定をしている人も過去にいたのですが、バイト長が4の文字には漢字の一部も含まれるので、少なくとも筆者の今回の用途には不向きのようでした。