お題
大文字アルファベット A〜Z のみを用いた文字列を左に 3 シフトさせたシーザー暗号を復号1するメソッドを書いてください。
たとえば「FIREFOX」を暗号化するとします。
暗号化ではアルファベットを左に 3 シフトさせるので,
A B C D E F G H I …
の系列を見ると,F は C に変換すればいいと判ります。
他の文字も同様にすると最終的に暗号文「CFOBCLU」が得られます。
作りたいメソッドはこの「CFOBCLU」を「FIREFOX」に戻すものです:
p decode_caesar("CFOBCLU") # => "FIREFOX"
コード
def decode_caesar(coded)
coded.chars.map{ |char| (char.ord + 3).chr }.join
end
文字ごとにバラバラにして,文字のコードを取得し,3 を足してそれをコードとする文字を得たあと,結合して返しています。
問題点
上のコードは不適切なのですが、実はこの記事の「お題」はシーザー暗号を十分には説明しておらず,このお題で初めてシーザー暗号を知ったのであれば,このようなコードを書くのは無理もないことです(よってバツを付けるわけにはいきません)。
この記事を書いたのは,シーザー暗号の復号として,上のようなコードがたくさん投稿されているのを見たからです。
何がまずいのでしょうか。
暗号化の際,左に 3 シフトするのですが,A〜C の 3 文字は X〜Z に変換することになっています。つまり,シーザー暗号は
変換前 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
変換後 X Y Z A B C D E F G H I J K L M N O P Q R S T U V W
という変換を施すものなのです。
コンピューター用語としては,このような操作は「シフト(shift)」ではなく「ローテート(rotate)」つまり回転と呼ぶことが多いようです2。
上に掲げたコードはこのことを考慮していないため,「SAFARI」を暗号化した「PXCXOF」を復号すると
p decode_caesar("PXCXOF") # => "S[F[RI"
となってしまいます。
改善
文字コードに単純に 3 を足すのではダメということが分かりました。どうすればいいでしょうか。
路線継承
元のコードの路線をできるだけ継承することを考えてみます。
すぐに思いつくのは,A〜W と X〜Z で場合分けすることです。
しかし,それだとシフト量が変わった場合の修正がスマートではなさそうですね。
数学的なアプローチとしては,剰余(整数の割り算の余り)を取る,ということが考えられます。
def decode_caesar(coded)
coded.chars.map do |char|
((char.ord - "A".ord + 3) % 26 + "A".ord).chr
end.join
end
ずいぶんややこしい式になりました。
真打登場
シーザー暗号は単一換字式暗号と呼ばれるものの一種です。
単一換字式暗号は,A や B や C や D といったアルファベットがそれぞれ何に変換されるかが固定されています3。
このような変換のためには専用のメソッド String#tr が用意されています4。
これを使うと復号のメソッドは
def decode_caesar(coded)
coded.tr("XYZABCDEFGHIJKLMNOPQRSTUVW", "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
end
と書けます(暗号化なら,第一引数と第二引数を入れ替えるだけ)。
ループを書く必要すらなく,メソッドを 1 回呼び出すだけで済むという簡潔さです。
でも,なんかこれ,引数がゴチャゴチャしてますよね。
私は間違わずにタイプできる気がしませんし,見ても瞬時には正しいことが分かりません。
スジ悪です。こんな書き方はやめておきましょう。
改良
String#tr
の引数は,引き続く部分をハイフンで繋げて書くことができます。
よって,さきほどのコードは
def decode_caesar(coded)
coded.tr("X-ZA-W", "A-Z")
end
と書き直せます。これなら安心ですね。
あるいは
def decode_caesar(coded)
coded.tr("A-Z", "D-ZA-C")
end
でもいいでしょう。
さて,シフト数が 3 で固定ならこれでいいわけですが,変わりうる場合はどうしたものでしょうか。
シフト数を自由に
いろんなやり方があると思いますが、まず
[*"A".."Z"]
# => ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
という「全文字の配列」を用意しておき,これを Array#rotate で回転させてやる,ということが考えられます5。
Array#rotate
の動作は
p ["a", "b", "c", "d", "e"].rotate(3)
# => ["d", "e", "a", "b", "c"]
を見ていただくと分かりますね。
これを使うと
def decode_caesar(coded)
coded.tr("A-Z", [*"A".."Z"].rotate(3).join)
end
と書けます。
ただ,このメソッドが多数回呼ばれる場合,メソッド内で
[*"A".."Z"].rotate(3).join
を毎回やるのは,効率が悪すぎます。
性能が求められる場合,この部分はメソッド外に出して定数に入れておくことになるでしょう。
シフト数が引数で与えられる場合でも,[*"A".."Z"]
は定数に入れておくとよいかもしれません。
速度
引数として与えられる文字列が非常に長い場合,剰余を用いた方法よりも,tr
を用いたほうが速いはずです。
文字列が百万字あれば,いったん百万要素の配列が作られ,百万回のブロック呼び出しが生じるからです。
(こういう話はベンチマークテストの結果を示して書くべきなのですが,ここまで書いて力尽きました)