ゼロから学ぶGo言語プログラミング(13) A Tour of Go 61

f:id:belbo:20140616112930j:plain

A Tour of Goの続き。 今回は長くなったので、io.Readerの課題のみ。

61.Exercise: Rot13 Reader

画像に引き続き、組み込みのインターフェースを元に、独自の実装を行う課題、かな? 課題そのものはio.Reader型を元にして、ストリームにROT13の複合を施す独自の型を作る、というもの。

バイト列やストリームの扱いも、ファイルの処理をあまりして来なかったため曖昧。 Goはちょっとしたツールを作るのがとても楽を出来そうなので、そういう点でもこの辺りは早めに理解しておきたい。

ROT13はぼんやりしか理解してなかったので、解説からもリンクがあるROT13 - Wikipediaで確認。 ROT13はアルファベットのみを対象として、ABC順に13文字後ろへずらす、という処理。 アルファベットは全体で26文字なので、2回の適用で元の文字に戻る。 暗号化と複合が同一処理で、しかもそれは非常に単純な変換の場合、暗号と言っていいんだろうか。

この処理を実装するには、対象の文字がアルファベットであるかどうか判定し、そうであれば13文字ずらした結果を得る必要がある。 Goでのこの手の文字判定や処理は全然なので、色々調べる必要がありそう。

io.Readerインターフェースを確認すると、byteの並んだsliceを受け取って、intとerrorを返す、となっている。 課題で扱うのはアルファベットだけど、バイト列を扱うから[]byteなのか。

importには最初から strings パッケージが並んでる。 このパッケージはstrings.NewReader()が使われている。 io.Reader自体はバイト列を扱う汎用的なもののようなので、strings.NewReader()は恐らく、文字列のバイト列を扱うものっぽい。

インターフェースのコードだけでは、実装すべき戻り値 int や error の仕様がわからない。 解説では n は受け取ったバイト列の長さを けど、rot13Readerはio.Readerを内包していて、main()でもROT13された文字列を取ったstrings.NewReader()が引数になっている。 つまり r には文字列のバイト列を受け取ったio.Readerが渡されるわけだから、とりあえずこれをそのままスルーしてみる。

func (r13 *rot13Reader) Read(s []byte) (n int, e error) {
    n, e = r13.r.Read(s)
    return
}

すると"Lbh penpxrq gur pbqr!"と、ROT13された課題の文字列がそのまま出力された。 この時点で s には渡されたROT13のバイト列が入っている。 バイト列から位置文字ずつROT13での変換をかけて返せば、仕様は満たせるはず。 しかし、Goでそういった文字列の比較や変換をどうおこなうべきか分からないので、幾つか基本そうなコードをPlaygroundで書いて試してみる。

まず、文字同士の比較。

package main

import "fmt"

func main() {
    x := "c"
    y := "b"
 
    f := "\"%s\" > \"%s\" is %s."
    if x > y {
        fmt.Printf(f, x,y,"true")
    } else {
        fmt.Printf(f, x,y,"false")
    }
}

単純に文字同士の比較では、シンプルに大小が判定できました。 では、文字を変更してみます。

"あ" > "b" is true.

ひらがなや漢字でも、適切に判定できています。 Unicodeベースで扱われているんでしょうか。 ともかくこれで、渡された文字がアルファベットに含まれるかは、

c >= "A" && c <= "z"

で判定できることがわかりました。 しかしここからどう文字を13ずらすかに悩んで言語仕様を読み直すと、こんな内容が。

文字リテラル - Go言語仕様 - golang.jp

"日本語" // UTF-8 input text 日本語 // UTF-8 input text as a raw literal

なんと、Goではクォートの違いで文字のリテラルの扱いが違う。 シングルクォートの場合、Unicodeのコードポイントを表現する。 ダブルクォートの場合、Unicode文字列となる。

ということは、シングルクォートの文字リテラルは、Unicodeのコードポイントを加減算できるのでは?

package main

import "fmt"

func main() {
    x := 'A'
    fmt.Println(string(x+1)) // output "B"
}

シングルクォートで文字リテラルは、そのままだとUnicodeのコードポイント。 stringで文字列として出力すれば、Aから+1でBになっている。 これを使えばROT13も可能そうだ。

最終的に、このコードで課題の仕様を満たせた。

package main

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

type rot13Reader struct {
    r io.Reader
}

func (r13 *rot13Reader) Read(s []byte) (n int, e error) {
    n, e = r13.r.Read(s)
    for i:=0; i < n; i++ {
        if s[i] > 'A' && s[i] < 'z' {
            if s[i] > 'z' - 13 {
               s[i] = s[i]-13 
            } else {
               s[i] = s[i]+13 
            }
        }
    }
    return
}

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

しかし理解できていないところもちらほら。

Stringやbyteの関係。

なぜ末尾で"io.Copy(os.Stdout, &r)"としているのか。

結局、ストリームをきちんと理解しきれていないのが問題。 自分の課題として残し、今は先に進む。