【Ruby のまずいコード】leet

# お題

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

* `A` → `4`
* `G` → `6`
* `I` → `1`
* `O` → `0`
* `S` → `5`
* `Z` → `2`

たとえば `"GIANTS"` を与えると `"614NT5"` を返します。

このような文字列表現を [leet](https://ja.wikipedia.org/wiki/Leet) と呼ぶようです。
メールなどでふざけてアルファベットの一部を形の似た数字に置き換えたりするものですが，ここでお題として挙げた変換は leet の多様な置き換えのごく一部です。

# コード

実際に見たことのあるコードを思い出しながら例を二つ作ってみました。

## コード 1

```rb
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

```rb
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 から見ましょう。
文字ごとに回すループを

```rb
str.chars.each do |char|

end
```

と書いていますが，いったん `chars` で配列にしてから `each` しなくても，専用のメソッド [String#each_char](https://docs.ruby-lang.org/ja/3.0.0/method/String/i/each_char.html) を使って

```rb
str.each_char do |char|

end
```

と書けばいいでしょう。字数にして 1 字減っただけですが，改善前は若干まどろっこしい感じがします。

しかしなにより，コード 2 と比べれば複雑すぎないでしょうか。

## コード 2 の改善

次にコード 2 を見ます。
何を何に変換するかが，ハッシュ一つでコンパクトに表現されていて，コード 1 より少しく保守しやすいと思います。ここでいう保守とは，変換の仕方を変えることなどを意味します。

変換本体は正規表現と `gsub` をうまく使って 1 行で済ませていますが，もっと簡潔に書けます。
[String#gsub](https://docs.ruby-lang.org/ja/3.0.0/method/String/i/gsub.html) には第二引数にハッシュを与える用法があるのです。これを使うと

```rb
def leet(str)
  leet_table = {
    "A" => "4",
    "G" => "6",
    "I" => "1",
    "O" => "0",
    "S" => "5",
    "Z" => "2"
  }
  str.gsub(/[AGIOSZ]/, leet_table)
end
```

と書けます。

ところで，変換テーブルのハッシュをメソッドの中で定義するのは効率がよくありません。メソッドが呼ばれるたびにハッシュオブジェクトが生成されます。
`leet` メソッドが極めて多くの回数呼び出される場合はそういったことも考慮したほうがよいかもしれません。
メソッド外で定義するなら

```rb
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 でない（つまり無駄な重複がある）とも言えそうです。
以下のようにしてはどうでしょうか。

```rb
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](https://docs.ruby-lang.org/ja/3.0.0/method/Regexp/s/escape.html) を用いて

```rb
RE_LEET_CHARS = /[#{ LEET_TABLE.keys.join }]/
```

を

```rb
RE_LEET_CHARS = /[#{ Regexp.escape(LEET_TABLE.keys.join) }]/
```

に変えてやります。

## 最適なコード

「お題」の節に書いたとおり，leet にはさまざまなものがあります。`B` を `l3` とするなど，1 文字を 2 文字で表したりすることもあります。
しかし，今回のお題はすべて 1 文字を 1 文字に置き換えるものです。こういった変換を転字（transliteration）と呼ぶことがあります。
Ruby には専用のメソッド [String#tr](https://docs.ruby-lang.org/ja/3.0.0/method/String/i/tr.html) があるので，これを使うと以下のように簡潔に書けます。

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

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