お題
引数として与えられた文字列に以下の変換を施した文字列を返すメソッドを定義してください。
-
A
→4
-
G
→6
-
I
→1
-
O
→0
-
S
→5
-
Z
→2
たとえば "GIANTS"
を与えると "614NT5"
を返します。
このような文字列表現を leet と呼ぶようです。
メールなどでふざけてアルファベットの一部を形の似た数字に置き換えたりするものですが,ここでお題として挙げた変換は leet の多様な置き換えのごく一部です。
コード
実際に見たことのあるコードを思い出しながら例を二つ作ってみました。
コード 1
def leet(str)
result = ""
str.chars.each do |char|
case char
when "A"
result << "4"
when "G"
result << "6"
when "I"
result << "1"
when "O"
result << "0"
when "S"
result << "5"
when "Z"
result << "2"
else
result << char
end
end
result
end
コード 2
def leet(str)
leet_table = {
"A" => "4",
"G" => "6",
"I" => "1",
"O" => "0",
"S" => "5",
"Z" => "2"
}
str.gsub(/[AGIOSZ]/){ leet_table[$&] }
end
改善
コード 1 の改善
まずコード 1 から見ましょう。
文字ごとに回すループを
str.chars.each do |char|
end
と書いていますが,いったん chars
で配列にしてから each
しなくても,専用のメソッド String#each_char を使って
str.each_char do |char|
end
と書けばいいでしょう。字数にして 1 字減っただけですが,改善前は若干まどろっこしい感じがします。
しかしなにより,コード 2 と比べれば複雑すぎないでしょうか。
コード 2 の改善
次にコード 2 を見ます。
何を何に変換するかが,ハッシュ一つでコンパクトに表現されていて,コード 1 より少しく保守しやすいと思います。ここでいう保守とは,変換の仕方を変えることなどを意味します。
変換本体は正規表現と gsub
をうまく使って 1 行で済ませていますが,もっと簡潔に書けます。
String#gsub には第二引数にハッシュを与える用法があるのです。これを使うと
def leet(str)
leet_table = {
"A" => "4",
"G" => "6",
"I" => "1",
"O" => "0",
"S" => "5",
"Z" => "2"
}
str.gsub(/[AGIOSZ]/, leet_table)
end
と書けます。
ところで,変換テーブルのハッシュをメソッドの中で定義するのは効率がよくありません。メソッドが呼ばれるたびにハッシュオブジェクトが生成されます。
leet
メソッドが極めて多くの回数呼び出される場合はそういったことも考慮したほうがよいかもしれません。
メソッド外で定義するなら
LEET_TABLE = {
"A" => "4",
"G" => "6",
"I" => "1",
"O" => "0",
"S" => "5",
"Z" => "2"
}
def leet(str)
str.gsub(/[AGIOSZ]/, LEET_TABLE)
end
となるでしょう。
しかし,LEET_TABLE
を変更したときに gsub
の第一引数を変更するのを忘れそうで怖いですね。保守性の観点からは望ましくありません。DRY でない(つまり無駄な重複がある)とも言えそうです。
以下のようにしてはどうでしょうか。
LEET_TABLE = {
"A" => "4",
"G" => "6",
"I" => "1",
"O" => "0",
"S" => "5",
"Z" => "2"
}
RE_LEET_CHARS = /[#{ LEET_TABLE.keys.join }]/
def leet(str)
str.gsub(RE_LEET_CHARS, LEET_TABLE)
end
RE_LEET_CHARS
という定数名は,変換すべき文字を表す正規表現(Regular Expression)というつもりの命名です。
このコードは,ベンチマークテストでコード 2 より 3 割程度速いようです。
追記(2021-12-04)
この最後のコードは,変換すべき文字がアルファベットだけという前提です。
もし,-
(ハイフン)などのように,正規表現の [ ]
の中でメタ文字と解釈される文字が含まれていたらまずいことになります。
こういうケースにも対応するには,Regexp.escape を用いて
RE_LEET_CHARS = /[#{ LEET_TABLE.keys.join }]/
を
RE_LEET_CHARS = /[#{ Regexp.escape(LEET_TABLE.keys.join) }]/
に変えてやります。
最適なコード
「お題」の節に書いたとおり,leet にはさまざまなものがあります。B
を l3
とするなど,1 文字を 2 文字で表したりすることもあります。
しかし,今回のお題はすべて 1 文字を 1 文字に置き換えるものです。こういった変換を転字(transliteration)と呼ぶことがあります。
Ruby には専用のメソッド String#tr があるので,これを使うと以下のように簡潔に書けます。
def leet(str)
str.tr("AGIOSZ", "461052")
end
簡潔なだけでなく,速度面で前節の最後のコードと比べても数倍の速さです。