お題
文字列を文字種で分割するメソッドを書いてください。
つまり,
p divide_by_script("Rubyは楽しい") # => ["Ruby", "は", "楽", "しい"]
となるような。
漠然と「文字種」と書きましたが,Unicode の仕様でいう「スクリプト(script)」のことであるとしましょう。
script という単語は多義語ですが,ここでは「漢字」「平仮名」「片仮名」「ラテン文字」「アラビア文字」「チベット文字」といったもののことです1。
Unicode では,各文字に script プロパティーという値が割り当てられていて,たとえば以下のようになっています:
- 「亜」などの漢字:
Han
2 - 「あ」などの平仮名:
Hiragana
- 「ア」などの片仮名:
Katakana
- 「a」などのラテン文字:
Latin
- 「α」などのギリシャ文字:
Greek
「!」などの記号類は,文字学的には何のスクリプトにも含まれませんが,Unicode では script プロパティーを持っており,Common
という値が割り当てられています。
文字のスクリプトは,unicode-scripts という gem で知ることができます。
まず
gem install unicode-scripts
で gem をインストールしておけば
require "unicode/scripts"
p Unicode::Scripts.script("あ") # => "Hiragana"
のようにして使えます。
もちろん Gemfile と Bundler を使っても構いません。
コード
require "unicode/scripts"
def divide_by_script(str)
last_script = nil
result = []
str.each_char do |char|
script = Unicode::Scripts.script(char)
if last_script == script
result.last << char
else
result << char
last_script = script
end
end
result
end
少しだけ説明を加えると,
result.last << char
は,配列の最後の要素である String オブジェクトの末尾に破壊的に文字列 char
を結合しています。
一方,
result << char
は配列の末尾に文字列 char
を追加しています。
同じ演算子 <<
を使っていますが,レシーバーのクラスが違うので,全く別のメソッドです。やっていることは違います3。
改善
Ruby の Enumerable モジュールには,要素の並びを〈要素から算出される値が等しいものの連なり〉に分解する専用メソッドEnumerable#chunk があります4。
これを使うと処理が 1 行で書けてしまいます:
require "unicode/scripts"
def divide_by_script(str)
str.chars.chunk{ |char| Unicode::Scripts.script(char) }.map{ |script, chars| chars.join }
end
ここで,map
のブロックパラメーター script
は使っていないので,_
とかでいいのですが,読者に「スクリプトだよ」と分かっていただきやすいように書きました。
Ruby 2.7 以降なら番号指定ブロックパラメーターを使って
require "unicode/scripts"
def divide_by_script(str)
str.chars.chunk{ Unicode::Scripts.script(_1) }.map{ _2.join }
end
とも書けますね。