1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【初心者向け】Ruby のまずいコード 25 本Advent Calendar 2021

Day 4

【Ruby のまずいコード】leet

Last updated at Posted at 2021-12-03

お題

引数として与えられた文字列に以下の変換を施した文字列を返すメソッドを定義してください。

  • A4
  • G6
  • I1
  • O0
  • S5
  • Z2

たとえば "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 にはさまざまなものがあります。Bl3 とするなど,1 文字を 2 文字で表したりすることもあります。
しかし,今回のお題はすべて 1 文字を 1 文字に置き換えるものです。こういった変換を転字(transliteration)と呼ぶことがあります。
Ruby には専用のメソッド String#tr があるので,これを使うと以下のように簡潔に書けます。

def leet(str)
  str.tr("AGIOSZ", "461052")
end

簡潔なだけでなく,速度面で前節の最後のコードと比べても数倍の速さです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?