diff --git a/kadai3-1/tsuchinaga/.gitignore b/kadai3-1/tsuchinaga/.gitignore new file mode 100644 index 0000000..c38fa4e --- /dev/null +++ b/kadai3-1/tsuchinaga/.gitignore @@ -0,0 +1,2 @@ +.idea +*.iml diff --git a/kadai3-1/tsuchinaga/README.md b/kadai3-1/tsuchinaga/README.md new file mode 100644 index 0000000..213b856 --- /dev/null +++ b/kadai3-1/tsuchinaga/README.md @@ -0,0 +1,20 @@ +# 課題3-1 tsuchinaga + +## タイピングゲームを作ろう +* 標準出力に英単語を出す(出すものは自由) +* 標準入力から1行受け取る +* 制限時間内に何問解けたか表示する + +### ヒント +* 制限時間にはtime.After関数を用いる + * context.WithTimeoutでもよい +* select構文を用いる + * 制限時間と入力を同時に待つ + +## TODO +* [x] タイピングゲームができる + * [x] ランダムなアルファベットが3~6文字表示される + * [x] 標準入力にて解答を受け付ける + * [x] 表示と入力を繰り返す + * [x] 状態にかかわらず15秒で終了する + * [x] 終了時に、表示と入力の一致回数を表示する diff --git a/kadai3-1/tsuchinaga/go.mod b/kadai3-1/tsuchinaga/go.mod new file mode 100644 index 0000000..f6b152d --- /dev/null +++ b/kadai3-1/tsuchinaga/go.mod @@ -0,0 +1,3 @@ +module github.com/gopherdojo/dojo8/kadai3-1/tsuchinaga + +go 1.14 diff --git a/kadai3-1/tsuchinaga/main.go b/kadai3-1/tsuchinaga/main.go new file mode 100644 index 0000000..ec71278 --- /dev/null +++ b/kadai3-1/tsuchinaga/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "bufio" + "os" + "time" + + "github.com/gopherdojo/dojo8/kadai3-1/tsuchinaga/typinggame" +) + +func main() { + bs := bufio.NewScanner(os.Stdin) + bs.Split(bufio.ScanLines) + + g := typinggame.New(time.Now().UnixNano()) + + tCh := time.After(15 * time.Second) + for { + iCh := make(chan string) + go func() { + println(g.Next()) + bs.Scan() + iCh <- bs.Text() + }() + + select { + case ans := <-iCh: + g.Answer(ans) + case _ = <-tCh: + println("\nResult", g.Result()) + return + } + } +} diff --git a/kadai3-1/tsuchinaga/typinggame/typinggame.go b/kadai3-1/tsuchinaga/typinggame/typinggame.go new file mode 100644 index 0000000..db441bb --- /dev/null +++ b/kadai3-1/tsuchinaga/typinggame/typinggame.go @@ -0,0 +1,57 @@ +package typinggame + +import ( + "math/rand" + "sync" +) + +// New - 新しいタイピンゲームの生成 +func New(seed int64) Game { + return &game{ + r: rand.New(rand.NewSource(seed)), + } +} + +// Game - ゲームのinterface +type Game interface { + Next() string + Answer(ans string) + Result() int +} + +// game - ゲーム +type game struct { + r *rand.Rand // 乱数 + q string // 問題 + pass int // 正答数 + mutex sync.Mutex +} + +// Next - 次の問題 +func (g *game) Next() string { + g.mutex.Lock() + defer g.mutex.Unlock() + + g.q = "" + n := g.r.Intn(4) + 3 + for i := 0; i < n; i++ { + g.q += string('a' + g.r.Intn(24)) + } + return g.q +} + +// Answer - 回答入力 +func (g *game) Answer(ans string) { + g.mutex.Lock() + defer g.mutex.Unlock() + + if g.q == ans { + g.pass++ + } +} + +func (g *game) Result() int { + g.mutex.Lock() + defer g.mutex.Unlock() + return g.pass +} diff --git a/kadai3-1/tsuchinaga/typinggame/typinggame_test.go b/kadai3-1/tsuchinaga/typinggame/typinggame_test.go new file mode 100644 index 0000000..1eb003e --- /dev/null +++ b/kadai3-1/tsuchinaga/typinggame/typinggame_test.go @@ -0,0 +1,114 @@ +package typinggame + +import ( + "math/rand" + "testing" +) + +func Test_game_Next(t *testing.T) { + t.Parallel() + type fields struct { + r *rand.Rand + q string + pass int + } + tests := []struct { + name string + fields fields + want string + }{ + {name: "randの値が98の場合は3文字", fields: fields{r: rand.New(rand.NewSource(98))}, want: "nnn"}, + {name: "randの値が99の場合は4文字", fields: fields{r: rand.New(rand.NewSource(99))}, want: "hsso"}, + {name: "randの値が83の場合は5文字", fields: fields{r: rand.New(rand.NewSource(83))}, want: "jdree"}, + {name: "randの値が79の場合は6文字", fields: fields{r: rand.New(rand.NewSource(79))}, want: "wqtjme"}, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + g := &game{} + + // ロックのテスト + g.mutex.Lock() + go func() { + defer g.mutex.Unlock() + g.r = tt.fields.r + }() + + if got := g.Next(); got != tt.want { + t.Errorf("Next() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_game_Answer(t *testing.T) { + t.Parallel() + + type fields struct { + q string + pass int + } + type args struct { + ans string + } + tests := []struct { + name string + fields fields + args args + want int + }{ + {name: "qとansが一致している場合にpassが加算される", fields: fields{q: "foo", pass: 3}, args: args{ans: "foo"}, want: 4}, + {name: "qとansが一致していない場合はpassは変わらない", fields: fields{q: "foo", pass: 3}, args: args{ans: "foo"}, want: 4}, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + g := &game{} + + // ロックのテスト + g.mutex.Lock() + go func() { + defer g.mutex.Unlock() + g.q = tt.fields.q + g.pass = tt.fields.pass + }() + + g.Answer(tt.args.ans) + if g.pass != tt.want { + t.Errorf("game.pass = %v, want %v", g.pass, tt.want) + } + }) + } +} + +func Test_game_Result(t *testing.T) { + t.Parallel() + type fields struct { + pass int + } + tests := []struct { + name string + fields fields + want int + }{ + {name: "passが0なら0が返される", fields: fields{pass: 0}, want: 0}, + {name: "passが3なら3が返される", fields: fields{pass: 3}, want: 3}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := &game{} + + // ロックのテスト + g.mutex.Lock() + go func() { + defer g.mutex.Unlock() + g.pass = tt.fields.pass + }() + if got := g.Result(); got != tt.want { + t.Errorf("Result() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/kadai3-2/tsuchinaga/README.md b/kadai3-2/tsuchinaga/README.md new file mode 100644 index 0000000..96eae1f --- /dev/null +++ b/kadai3-2/tsuchinaga/README.md @@ -0,0 +1,19 @@ +# 課題3-2 tsuchinaga + +## 分割ダウンロードを行う +* Rangeアクセスを用いる +* いくつかのゴルーチンでダウンロードしてマージする +* エラー処理を工夫する + * golang.org/x/sync/errgourpパッケージなどを使ってみる +* キャンセルが発生した場合の実装を行う + +## TODO +* [x] 分割ダウンロードを行なう + * [x] ダウンロードするファイルをURLで指定できる + * [x] ダウンロードするファイルを指定していなかったらエラー + * [x] ダウンロードするファイルのURLが存在しなかったらエラー + * [x] 指定されたURLを分割してダウンロードできる + * [x] 指定範囲をダウンロードする + * [x] ダウンロードした結果をマージしてファイルに出力する + * [x] 並列ダウンロードしているうち、ひとつでも失敗したらエラーを出して終了 + * [x] SIGINTをキャッチして中断することができる diff --git a/kadai3-2/tsuchinaga/downloader/downloader.go b/kadai3-2/tsuchinaga/downloader/downloader.go new file mode 100644 index 0000000..c979e07 --- /dev/null +++ b/kadai3-2/tsuchinaga/downloader/downloader.go @@ -0,0 +1,185 @@ +package downloader + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "sync" +) + +type ProgressType string + +const ( + ProgressTypeDone = "done" + ProgressTypeFailed = "failed" + ProgressTypeNotice = "notice" +) + +type Progress struct { + Type ProgressType + Message string +} + +// New - 新しいダウンローダーを生成する +func New(url string) Downloader { + fileName := filepath.Base(url) + return &downloader{url: url, fileName: fileName} +} + +// Downloader - ダウンローダーのインターフェース +type Downloader interface { + Do(context.Context) chan Progress +} + +// downloader - ダウンローダー +type downloader struct { + ch chan Progress + url string + fileName string +} + +// Do - 分割しながらデータを取得する +func (d *downloader) Do(ctx context.Context) chan Progress { + // すでに結果を返すchanが作られていたらそれを返す + if d.ch != nil { + return d.ch + } + + d.ch = make(chan Progress) + go func() { + defer func() { d.ch <- Progress{Type: ProgressTypeDone} }() // 最後に必ずDoneが返される + // 分割に対応しているかを確認する + ranges, err := d.AcceptRanges() + if err != nil { + d.ch <- Progress{Type: ProgressTypeFailed, Message: err.Error()} + return + } + + totalSize, err := d.ContentLength() + if err != nil { + d.ch <- Progress{Type: ProgressTypeFailed, Message: err.Error()} + return + } + if totalSize <= 0 { + d.ch <- Progress{Type: ProgressTypeNotice, Message: "Content-Lengthが不明なため一括取得します"} + } + + var bytesList [][]byte + switch ranges { + case "none": // 分割に対応していない場合はgoroutine1つで取得する + if body, err := d.GetAll(ctx); err != nil { + d.ch <- Progress{Type: ProgressTypeFailed, Message: err.Error()} + return + } else { + bytesList = [][]byte{body} + d.ch <- Progress{Type: ProgressTypeNotice, Message: "ダウンロード完了"} + } + case "bytes": // 分割に対応している場合、4分割にして4つのgoroutineで取得する + var wg sync.WaitGroup + bytesList = make([][]byte, 4) + for i := 0; i < 4; i++ { + n := i + part := totalSize / 4 + wg.Add(1) + go func() { + defer wg.Done() + start := part * int64(n) + end := (part * int64(n+1)) - 1 + if n == 3 { + end = totalSize + } + + d.ch <- Progress{Type: ProgressTypeNotice, Message: fmt.Sprintf("分割ダウンロード(%d)開始", n+1)} + if body, err := d.GetPart(ctx, start, end); err != nil { + d.ch <- Progress{Type: ProgressTypeFailed, Message: err.Error()} + return + } else { + bytesList[n] = body + d.ch <- Progress{Type: ProgressTypeNotice, Message: fmt.Sprintf("分割ダウンロード(%d)完了", n+1)} + } + }() + } + wg.Wait() + } + + out, err := os.Create(d.fileName) + if err != nil { + d.ch <- Progress{Type: ProgressTypeFailed, Message: err.Error()} + return + } + for _, b := range bytesList { + if _, err := out.Write(b); err != nil { + d.ch <- Progress{Type: ProgressTypeFailed, Message: err.Error()} + return + } + } + if err := out.Close(); err != nil { + d.ch <- Progress{Type: ProgressTypeFailed, Message: err.Error()} + return + } + }() + return d.ch +} + +func (d *downloader) GetAll(ctx context.Context) (bytes []byte, err error) { + req, err := http.NewRequestWithContext(ctx, "GET", d.url, nil) + if err != nil { + return nil, err + } + cli := new(http.Client) + res, err := cli.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + return body, nil +} + +func (d *downloader) GetPart(ctx context.Context, start, end int64) (bytes []byte, err error) { + req, err := http.NewRequestWithContext(ctx, "GET", d.url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end)) + cli := new(http.Client) + res, err := cli.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + return body, nil +} + +// ContentLength - url先のコンテンツの長さを取得 +func (d *downloader) ContentLength() (int64, error) { + res, err := http.Head(d.url) + if err != nil { + return 0, err + } + return res.ContentLength, nil +} + +// AcceptRanges - 範囲リクエストの設定を確認する +func (d *downloader) AcceptRanges() (string, error) { + res, err := http.Head(d.url) + if err != nil { + return "", err + } + ars := res.Header["Accept-Ranges"] + if len(ars) == 0 { + return "none", nil + } else { + return ars[0], nil + } +} diff --git a/kadai3-2/tsuchinaga/downloader/downloader_test.go b/kadai3-2/tsuchinaga/downloader/downloader_test.go new file mode 100644 index 0000000..e62e7d1 --- /dev/null +++ b/kadai3-2/tsuchinaga/downloader/downloader_test.go @@ -0,0 +1,96 @@ +package downloader + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +func Test_downloader_ContentLength(t *testing.T) { + t.Parallel() + tests := []struct { + name string + contentLength string + httpStatusCode int + want int64 + wantErr bool + }{ + {name: "contentLengthがなければ-1が返る", contentLength: "", httpStatusCode: http.StatusOK, want: -1}, + {name: "contentLengthが0なら0が返る", contentLength: "0", httpStatusCode: http.StatusOK, want: 0}, + {name: "contentLengthが10なら10が返る", contentLength: "10", httpStatusCode: http.StatusOK, want: 10}, + {name: "httpStatusが200以外でもcontentLengthはそのまま返される", contentLength: "10", httpStatusCode: http.StatusNotFound, want: 10}, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if tt.contentLength != "" { + w.Header().Add("Content-Length", tt.contentLength) + } + w.WriteHeader(tt.httpStatusCode) + _, _ = w.Write([]byte{}) + })) + + d := &downloader{url: ts.URL} + got, err := d.ContentLength() + if (err != nil) != tt.wantErr { + t.Errorf("ContentLength() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ContentLength() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_downloader_AcceptRanges(t *testing.T) { + t.Parallel() + tests := []struct { + name string + acceptRanges string + httpStatusCode int + want string + wantErr bool + }{ + {name: "acceptRangesがなければ-1が返る", acceptRanges: "", httpStatusCode: http.StatusOK, want: "none"}, + {name: "acceptRangesが0なら0が返る", acceptRanges: "none", httpStatusCode: http.StatusOK, want: "none"}, + {name: "acceptRangesが10なら10が返る", acceptRanges: "byte", httpStatusCode: http.StatusOK, want: "byte"}, + {name: "httpStatusが200以外でもacceptRangesはそのまま返される", acceptRanges: "byte", httpStatusCode: http.StatusNotFound, want: "byte"}, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if tt.acceptRanges != "" { + w.Header().Add("Accept-Ranges", tt.acceptRanges) + } + w.WriteHeader(tt.httpStatusCode) + _, _ = w.Write([]byte{}) + })) + d := &downloader{url: ts.URL} + got, err := d.AcceptRanges() + if (err != nil) != tt.wantErr { + t.Errorf("AcceptRanges() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("AcceptRanges() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNew(t *testing.T) { + t.Parallel() + url := "http://example.com/foo/bar" + want := &downloader{url: url} + got := New(url) + + if !reflect.DeepEqual(got, want) { + t.Errorf("New() = %v, want %v", got, want) + } +} diff --git a/kadai3-2/tsuchinaga/go.mod b/kadai3-2/tsuchinaga/go.mod new file mode 100644 index 0000000..e9fab16 --- /dev/null +++ b/kadai3-2/tsuchinaga/go.mod @@ -0,0 +1,3 @@ +module github.com/gopherdojo/dojo8/kadai3-2/tsuchinaga + +go 1.14 diff --git a/kadai3-2/tsuchinaga/main.go b/kadai3-2/tsuchinaga/main.go new file mode 100644 index 0000000..327fe43 --- /dev/null +++ b/kadai3-2/tsuchinaga/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "flag" + "github.com/gopherdojo/dojo8/kadai3-2/tsuchinaga/downloader" + "log" + "os" + "os/signal" +) + +func main() { + flag.Parse() + url := flag.Arg(0) + + ctx, cFunc := context.WithCancel(context.Background()) + + sigCh := make(chan os.Signal) + signal.Notify(sigCh, os.Interrupt) + go func() { + for range sigCh { + println("すべての処理を中断して終了します") + cFunc() + close(sigCh) + os.Exit(2) + } + }() + + dCh := downloader.New(url).Do(ctx) + for progress := range dCh { + switch progress.Type { + case downloader.ProgressTypeFailed, downloader.ProgressTypeNotice: + log.Println(progress.Message) + case downloader.ProgressTypeDone: + log.Println("完了") + return + default: + log.Println("想定外のtype") + } + } +}