A Tour of Go の Exercise: rot13Reader 解答例

はじめに

Go 言語基礎文法最速マスターにて出題されている演習課題「Exercise: rot13Reader(ロットサーティーンリーダー)」の答えに辿り着くまでの手順と解答例の解説を記事にしてみました。シーザー暗号、ROT13 の計算式とロジックなどについても簡単に説明しています。

課題

課題の目的は io.Reader を実装し、io.Reader で ROT13 換字式暗号を全てのアルファベット文字に適用して読み出すように rot13Reader を実装することです。

package main

import (
	"io"
	"os"
	"strings"
)

type rot13Reader struct {
	r io.Reader
}

func main() {
	s := strings.NewReader("Lbh penpxrq gur pbqr!")
	r := rot13Reader{s}
	io.Copy(os.Stdout, &r)
}

解答例

package main

import (
	"io"
	"os"
	"strings"
)

type rot13Reader struct {
	r io.Reader
}

func (r *rot13Reader) Read(p []byte) (n int, e error) {
	n, e = r.r.Read(p)
	for i := range p {
		p[i] = rot13(p[i])
	}
	return
}

func rot13(n byte) byte {
	switch {
	case ('A' <= n && n <= 'Z'):
		return (n - 'A' + 13) % 26 + 'A'
	case ('a' <= n && n <= 'z'):
		return (n - 'a' + 13) % 26 + 'a'
	default:
		return n
	}
}

func main() {
	s := strings.NewReader("Lbh penpxrq gur pbqr!")
	r := rot13Reader{s}
	io.Copy(os.Stdout, &r)
}

実行結果は下記になります。

You cracked the code!

解説

初めに、ROT13 について簡単に説明します。

ROT13 は「Rotate by 13 places」の略称で、アルファベットの各文字を 13 文字後の文字に置換する 単換字式暗号 の一種です。

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz // 変換前
NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm // 変換後

シーザー暗号 ってやつですね。

シーザー暗号は古代共和制ローマ末期の軍人、政治家、文筆家である ガイウス・ユリウス・カエサル が使っていた暗号です。

カエサル(Caesar)は、英語でシーザーと読むのでこの名前が付きました。

この暗号は、例えば「Never Gonna Give You Up」という文字列を ROT13 で暗号化すると下記のように変換されます。

Never Gonna Give You Up // 変換前
Arire Tbaan Tvir Lbh Hc // 変換後

とても単純な暗号なので、現代では脱出ゲームの謎解きや子供のクイズに使われたりしています。

では、課題のソースコードを読み解いていきます。

9 – 11 行目、type で struct{} 型を rot13Reader という名前で 型宣言 しています、io.Reader インターフェースをラップしていますね。

type rot13Reader struct {
	r io.Reader
}

13 – 17 行目、main 関数では strings.NewReader 関数で io.Reader インターフェースを実装した ポインタ を変数 s に格納、さらに rot13Reader 型で変数 r を定義、io.Copy 関数で標準出力していますね。

func main() {
	s := strings.NewReader("Lbh penpxrq gur pbqr!")
	r := rot13Reader{s}
	io.Copy(os.Stdout, &r)
}

io.Copy 関数はデータを io.Reader で読み込み、EOF かエラーが発生するまで io.Writer にコピーします。そして、コピーしたバイト数とコピー中に発生したエラーを返します。

さて、実装を進める前に ROT13 のアルゴリズムをどのようにプログラムに落とし込むか考える必要があります。

前述した通り、ROT13 はアルファベットの各文字を 13 文字後の文字に置換する暗号です。

ということは、ASCII コードに 13 を加算して出力すれば良いですね。

fmt.Println(string('a' + 13)) // n
fmt.Println(string('A' + 13)) // N

ありゃ、でも n 以降だと変になってしまう。

fmt.Println(string('n' + 13)) // {
fmt.Println(string('N' + 13)) // [

つまり、ASCII コードの 65 – 95(A 〜 Z)、97 – 122(a 〜 z)を循環させる計算式が必要ということがわかります。

答えは、文字 xn 個のシフトで暗号化する計算式は (x + n) mod 26 です。

mod剰余演算(モジュロ)のことです、簡単に言うと割り算の余りのことです。

試しに x(120)と Y(89)を変換してみます。

x := ('x' - 'a' + 13) % 26 + 'a'
fmt.Println(string(x)) // k
y := ('Y' - 'A' + 13) % 26 + 'A'
fmt.Println(string(y)) // L

え、剰余演算子 % のオペランド 'a''A' には何の意味があるんじゃいって?

それはアルファベット文字の ASCII コードが 0 から始まっていないからですね、開始値を加算してあげる必要があったからです。

下記のような Switch を組み合わせた ROT13 の関数を実装すれば良いことがイメージできると思います。

func rot13(n byte) byte {
	switch {
	case ('A' <= n && n <= 'Z'):
		return (n - 'A' + 13) % 26 + 'A'
	case ('a' <= n && n <= 'z'):
		return (n - 'a' + 13) % 26 + 'a'
	default:
		return n
	}
}

あとは Read メソッドを呼び出してバイト列を読み込み、for-range で各文字に対して rot13 関数を実行して暗号化するだけです。

func (r *rot13Reader) Read(p []byte) (n int, e error) {
	n, e = r.r.Read(p)
	for i := range p {
		p[i] = rot13(p[i])
	}
	return
}

今迄を纏めるとこんな感じになります。

package main

import (
	"io"
	"os"
	"strings"
)

type rot13Reader struct {
	r io.Reader
}

func (r *rot13Reader) Read(p []byte) (n int, e error) {
	n, e = r.r.Read(p)
	for i := range p {
		p[i] = rot13(p[i])
	}
	return
}

func rot13(n byte) byte {
	switch {
	case ('A' <= n && n <= 'Z'):
		return (n - 'A' + 13) % 26 + 'A'
	case ('a' <= n && n <= 'z'):
		return (n - 'a' + 13) % 26 + 'a'
	default:
		return n
	}
}

func main() {
	s := strings.NewReader("Lbh penpxrq gur pbqr!")
	r := rot13Reader{s}
	io.Copy(os.Stdout, &r)
}

実行すると下記のように出力されます。

You cracked the code!

大丈夫だ、問題ない。

以上です。

おわりに

ROT13 は 1980 年代に始まったネタバレ防止対策でした、解読されることを前提とした素晴らしい工夫ですね。

また、なぜ 13 なのかというとエンコードとデコードを同じ方法でできる数字が 13 だったからですね。