Goについて勉強中なのですが、自分はこれまであまり実装を行ったことがなく、チームビルディングやビジネス寄りを担当していたことが多いため、とにかくなんでもいいから書いてみようと思い、ゆめみ株式会社のサーバーサイド採用試験サンプルで練習してみました。
解いていく中で、ここもっと改善できそうだなとか、ここの仕様は次にコード見た時忘れちゃってそうだなというところはコメントに残して、あとはそんなにコメントしないように意識しました。
まだまだ他の人の解法記事などを読んでいると、自分のコードはもっさりした関数になっていてかっこよくないなと思ってしまいます。
すこしでもかっこいいコードが書けるようにもっと場数踏まなきゃと思います。
問題から何が必要かを考えるフェーズ
まず、問題文を見て、どういった処理が必要かを洗い出しました。
- CSVファイルの読み込み
- 各プレイヤーの平均スコアを算出
- 平均スコアの降順に並び替え&順位づけ
- 10位までのプレーヤーを抽出
- CSVファイルに書き出し
これぐらいの単位で切り分けて関数の大枠を作成し、それぞれの実装の詳細についてはコメントでブレイクダウンしていって実装していきました。
以下のような感じです。
1. CSVファイルの読み込み
// main関数は省略
func readCsv() {
// ファイルパスは引数で受け取る
// ファイル開く
// ファイルを読み込むが、ヘッダーはいらないからスキップ
// ヘッダー以外を読み込み、スライスに入れていく
// スライスを返す
}
より詳細に機能を表現したかったため、実装後関数名を変えています。
// main関数は省略
func readCsvBodyAndConvertToSlice(filepath *string) [][]string {
file, err := os.Open(*filepath)
if err != nil {
log.Fatalln(err)
}
defer file.Close()
r := csv.NewReader(file)
// ヘッダーはスキップ
_, err = r.Read()
if err != nil {
log.Fatalln(err)
}
var rows [][]string
for {
row, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
log.Fatalln(err)
}
rows = append(rows, row)
}
return rows
}
2. 各プレイヤーの平均スコアを算出
上記のCSV読み込みの関数で [][]string
で返していますが、スコアを扱うには int に変換して扱いたいので、構造体にした方が楽だなと判断し、以下の関数を作成しました。まあただの型変換です。
type playLog struct {
createdAt string
playerId string
score int
}
func convertToPlayLogs(rows [][]string) []*playLog {
playLogs := []*playLog{}
for _, row := range rows {
score, err := strconv.Atoi(row[2])
if err != nil {
log.Fatalln(err)
}
pl := playLog{createdAt: row[0], playerId: row[1], score: score}
playLogs = append(playLogs, &pl)
}
return playLogs
}
では平均値の算出を行います。
この処理をブレイクダウンしていくと以下のようになります。
func calculateMeanScore(playLogs []*playLog) {
// 各プレイヤーのプレイ回数を集計する
// 各プレイヤーのスコアの合計を計算する
// スコアの合計をプレイ回数で割って平均スコア(mean_score)を算出する(少数の四捨五入も必要)
// player_idとmean_scoreを持つ構造体のスライスを返す
}
実装途中で、player_idとmean_scoreを持つ構造体が欲しいなと思い、さらに次のステップで順位が必要になるのはわかっていたので、meanScoreRankという構造体を用意しておきました。
また、今回も関数名がより詳細になってます。
type meanScoreRank struct {
rank int
player_id string
mean_score int
}
func calculateMeanScoreAndConvertToMeanScoreRank(playLogs []*playLog) []meanScoreRank {
scoreMap := make(map[string]int)
countMap := make(map[string]int)
for _, pl := range playLogs {
scoreMap[pl.playerId] += pl.score
countMap[pl.playerId]++
}
// 集計したscoreを足したscoreの数で割って平均値を算出する
meanScoreRanks := []meanScoreRank{}
for pid, pscore := range scoreMap {
meanScore := int(math.Round(float64(pscore) / float64(countMap[pid])))
msr := meanScoreRank{1, pid, meanScore}
meanScoreRanks = append(meanScoreRanks, msr)
}
return meanScoreRanks
}
3. 平均スコアの降順に並び替え&順位づけ
並び替えに関しては、Slice もしくは SliceStable があるが、毎回同じスコアのプレイヤーの並び順が変わると単体テストが面倒になりそうなので、SliceStableを採用しました。参考記事(GoのSliceをSortする(sort.Sliceとsort.SliceStable))
順位づけに関しては「順位づけ アルゴリズム」などで調べたのですが、一番単純な総当たり比較で行っています。
ただ、ここに関してはもうちょっと勉強して、インデックスを平均スコアとして処理する方法などにすればもうちょっと良くすることができるかなと思っています。アルゴリズムの勉強ももうちょっとしたいですね。
// ここ計算量多そうだからちょっとアルゴリズム考え直した方がいいかも。
func sortOrderByMeanScoreAndRank(msr []meanScoreRank) []meanScoreRank {
sort.SliceStable(msr, func(i, j int) bool {
return msr[i].mean_score > msr[j].mean_score
})
// 計算量O(n^2)か、、、
for i := 0; i < len(msr); i++ {
for j := i; j < len(msr); j++ {
if msr[i].mean_score > msr[j].mean_score {
msr[j].rank += 1
}
}
}
return msr
}
4. 10位までのプレーヤーを抽出
func extractTopTenScoreRank(srArr []meanScoreRank) []meanScoreRank {
// 必ず上から10個目までは抽出
// 10個以上の出力になるときは必ず、11個目以降が10個目のランクと同じとき
}
func extractTopTenScoreRank(srArr []meanScoreRank) []meanScoreRank {
if len(srArr) == 0 {
return srArr
}
result := []meanScoreRank{}
for i, v := range srArr {
// 必ず上から10個目までは出力
if i < 10 {
result = append(result, v)
}
// 10個以上の出力になるときは必ず、11個目以降が10個目のランクと同じとき
if i >= 10 && v.rank == srArr[9].rank {
result = append(result, v)
}
}
return result
}
5. CSVファイルに書き出し
func outputMeanScoreRanksToCsv(meanScoreRanks []meanScoreRank) {
// ヘッダーを追加
// スコアを追加
// ファイル名指定
// 出力
}
func outputMeanScoreRanksToCsv(meanScoreRanks []meanScoreRank) {
var records [][]string
csvHeader := []string{"rank", "player_id", "mean_score"}
records = append(records, csvHeader)
for _, v := range meanScoreRanks {
strRank := strconv.Itoa(v.rank)
strMeanScore := strconv.Itoa(v.mean_score)
record := []string{strRank, v.player_id, strMeanScore}
records = append(records, record)
}
timestamp := time.Now().Format("2006-01-02T15:04:05Z07:00")
// TODO 出力先が変わったらどうする?その時のために、環境変数か何かに切り出しておく?
file, err := os.Create("./result/get_ranking game_score_log_" + timestamp + ".csv")
if err != nil {
log.Fatalln(err)
}
w := csv.NewWriter(file)
if err := w.WriteAll(records); err != nil {
log.Fatalln("error writing record to csv:", err)
}
}
全実装
package main
import (
"encoding/csv"
"flag"
"io"
"log"
"math"
"os"
"sort"
"strconv"
"time"
)
type playLog struct {
createdAt string
playerId string
score int
}
type meanScoreRank struct {
rank int
player_id string
mean_score int
}
func main() {
inputFilePath := flag.String("filepath", "input_data/game_score.csv", "input file path") // CSVからJSONに仕様が変わる想定もする??
flag.Parse()
rows := readCsvBodyAndConvertToSlice(inputFilePath)
playLogs := convertToPlayLogs(rows)
meanScores := calculateMeanScoreAndConvertToMeanScoreRank(playLogs)
meanScoreRanks := sortOrderByMeanScoreAndRank(meanScores)
topTenMeanScoreRanks := extractTopTenScoreRank(meanScoreRanks)
outputMeanScoreRanksToCsv(topTenMeanScoreRanks)
}
func readCsvBodyAndConvertToSlice(filepath *string) [][]string {
file, err := os.Open(*filepath)
if err != nil {
log.Fatalln(err)
}
defer file.Close()
r := csv.NewReader(file)
// ヘッダーはスキップ
_, err = r.Read()
if err != nil {
log.Fatalln(err)
}
var rows [][]string
for {
row, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
log.Fatalln(err)
}
rows = append(rows, row)
}
return rows
}
func convertToPlayLogs(rows [][]string) []*playLog {
playLogs := []*playLog{}
for _, row := range rows {
score, err := strconv.Atoi(row[2])
if err != nil {
log.Fatalln(err)
}
pl := playLog{createdAt: row[0], playerId: row[1], score: score}
playLogs = append(playLogs, &pl)
}
return playLogs
}
func calculateMeanScoreAndConvertToMeanScoreRank(playLogs []*playLog) []meanScoreRank {
scoreMap := make(map[string]int)
countMap := make(map[string]int)
for _, pl := range playLogs {
scoreMap[pl.playerId] += pl.score
countMap[pl.playerId]++
}
// 集計したscoreを足したscoreの数で割って平均値を算出する
meanScoreRanks := []meanScoreRank{}
for pid, pscore := range scoreMap {
meanScore := int(math.Round(float64(pscore) / float64(countMap[pid])))
msr := meanScoreRank{1, pid, meanScore}
meanScoreRanks = append(meanScoreRanks, msr)
}
return meanScoreRanks
}
// ここ計算量多そうだからちょっとアルゴリズム考え直した方がいいかも。
func sortOrderByMeanScoreAndRank(msr []meanScoreRank) []meanScoreRank {
sort.SliceStable(msr, func(i, j int) bool {
return msr[i].mean_score > msr[j].mean_score
})
// 計算量O(n^2)か、、、
for i := 0; i < len(msr); i++ {
for j := i; j < len(msr); j++ {
if msr[i].mean_score > msr[j].mean_score {
msr[j].rank += 1
}
}
}
return msr
}
func extractTopTenScoreRank(srArr []meanScoreRank) []meanScoreRank {
if len(srArr) == 0 {
return srArr
}
result := []meanScoreRank{}
for i, v := range srArr {
// 必ず上から10個目までは抽出
if i < 10 {
result = append(result, v)
}
// 10個以上の出力になるときは必ず、11個目以降が10個目のランクと同じとき
if i >= 10 && v.rank == srArr[9].rank {
result = append(result, v)
}
}
return result
}
func outputMeanScoreRanksToCsv(meanScoreRanks []meanScoreRank) {
var records [][]string
csvHeader := []string{"rank", "player_id", "mean_score"}
records = append(records, csvHeader)
for _, v := range meanScoreRanks {
strRank := strconv.Itoa(v.rank)
strMeanScore := strconv.Itoa(v.mean_score)
record := []string{strRank, v.player_id, strMeanScore}
records = append(records, record)
}
timestamp := time.Now().Format("2006-01-02T15:04:05Z07:00")
// TODO 出力先が変わったらどうする?その時のために、環境変数か何かに切り出しておく?
file, err := os.Create("./result/get_ranking game_score_log_" + timestamp + ".csv")
if err != nil {
log.Fatalln(err)
}
w := csv.NewWriter(file)
if err := w.WriteAll(records); err != nil {
log.Fatalln("error writing record to csv:", err)
}
}
単体テスト
長くなってしまって恐縮なんですが、一応単体テストも書いておきます。
足りない部分もありますが、実装で確認したかったところだけ記述しました。
package main
import (
"fmt"
"reflect"
"testing"
)
func Test_readCsvBodyAndConvertToSlice(t *testing.T) {
cases := []struct {
msg string
a string
expected [][]string
}{
{
msg: "normal pattern",
a: "test_data/game_score_test.csv",
expected: [][]string{
{"2021/01/01 12:00", "player0001", "12345"},
{"2021/01/01 12:00", "player0002", "10000"},
},
},
}
for k, v := range cases {
t.Run(fmt.Sprintf("#%d %s", k, v.msg), func(t *testing.T) {
got := readCsvBodyAndConvertToSlice(&v.a)
if !reflect.DeepEqual(got, v.expected) {
t.Errorf("\nwant:\n %v, \ngot:\n %v", v.expected, got)
}
})
}
}
func Test_calculateMeanScoreAndConvertToMeanScoreRank(t *testing.T) {
cases := []struct {
msg string
a []*playLog
expected []meanScoreRank
}{
{
msg: "normal pattern",
a: []*playLog{
{"2021/01/01 12:00", "player0001", 10000},
},
expected: []meanScoreRank{
{1, "player0001", 10000},
},
},
{
msg: "float pattern",
a: []*playLog{
{"2021/01/01 12:00", "player0001", 10000},
{"2021/01/01 12:00", "player0001", 10001},
},
expected: []meanScoreRank{
{1, "player0001", 10001},
},
},
}
for k, v := range cases {
t.Run(fmt.Sprintf("#%d %s", k, v.msg), func(t *testing.T) {
got := calculateMeanScoreAndConvertToMeanScoreRank(v.a)
if !reflect.DeepEqual(got, v.expected) {
t.Errorf("\nwant:\n %v, \ngot:\n %v", v.expected, got)
}
})
}
}
func Test_sortOrderByMeanScoreAndRank(t *testing.T) {
cases := []struct {
msg string
a []meanScoreRank
expected []meanScoreRank
}{
{
msg: "normal pattern",
a: []meanScoreRank{
{1, "player0001", 10000},
{1, "player0002", 9000},
{1, "player0003", 8000},
{1, "player0004", 7000},
{1, "player0005", 6000},
{1, "player0006", 5000},
{1, "player0007", 4000},
{1, "player0008", 3000},
{1, "player0009", 2000},
{1, "player0010", 1000},
},
expected: []meanScoreRank{
{1, "player0001", 10000},
{2, "player0002", 9000},
{3, "player0003", 8000},
{4, "player0004", 7000},
{5, "player0005", 6000},
{6, "player0006", 5000},
{7, "player0007", 4000},
{8, "player0008", 3000},
{9, "player0009", 2000},
{10, "player0010", 1000},
},
},
{
msg: "same rank pattern",
a: []meanScoreRank{
{1, "player0001", 10000},
{1, "player0002", 10000},
{1, "player0003", 10000},
{1, "player0004", 7000},
{1, "player0005", 6000},
{1, "player0006", 5000},
{1, "player0007", 5000},
{1, "player0008", 5000},
{1, "player0009", 5000},
{1, "player0010", 1000},
},
expected: []meanScoreRank{
{1, "player0001", 10000},
{1, "player0002", 10000},
{1, "player0003", 10000},
{4, "player0004", 7000},
{5, "player0005", 6000},
{6, "player0006", 5000},
{6, "player0007", 5000},
{6, "player0008", 5000},
{6, "player0009", 5000},
{10, "player0010", 1000},
},
},
{
msg: "random pattern",
a: []meanScoreRank{
{1, "player0001", 10000},
{1, "player0002", 5000},
{1, "player0003", 7000},
{1, "player0004", 10000},
{1, "player0005", 6000},
{1, "player0006", 5000},
{1, "player0007", 1000},
{1, "player0008", 5000},
{1, "player0009", 10000},
{1, "player0010", 5000},
},
expected: []meanScoreRank{
{1, "player0001", 10000},
{1, "player0004", 10000},
{1, "player0009", 10000},
{4, "player0003", 7000},
{5, "player0005", 6000},
{6, "player0002", 5000},
{6, "player0006", 5000},
{6, "player0008", 5000},
{6, "player0010", 5000},
{10, "player0007", 1000},
},
},
{
msg: "more ten record pattern",
a: []meanScoreRank{
{1, "player0001", 10000},
{1, "player0002", 5000},
{1, "player0003", 7000},
{1, "player0004", 10000},
{1, "player0005", 6000},
{1, "player0006", 5000},
{1, "player0007", 1000},
{1, "player0008", 5000},
{1, "player0009", 10000},
{1, "player0010", 5000},
{1, "player0011", 900},
{1, "player0012", 800},
{1, "player0013", 700},
{1, "player0014", 600},
{1, "player0015", 500},
},
expected: []meanScoreRank{
{1, "player0001", 10000},
{1, "player0004", 10000},
{1, "player0009", 10000},
{4, "player0003", 7000},
{5, "player0005", 6000},
{6, "player0002", 5000},
{6, "player0006", 5000},
{6, "player0008", 5000},
{6, "player0010", 5000},
{10, "player0007", 1000},
{11, "player0011", 900},
{12, "player0012", 800},
{13, "player0013", 700},
{14, "player0014", 600},
{15, "player0015", 500},
},
},
}
for k, v := range cases {
t.Run(fmt.Sprintf("#%d %s", k, v.msg), func(t *testing.T) {
got := sortOrderByMeanScoreAndRank(v.a)
if !reflect.DeepEqual(got, v.expected) {
t.Errorf("\nwant:\n %v, \ngot:\n %v", v.expected, got)
}
})
}
}
func Test_extractTopTenScoreRank(t *testing.T) {
cases := []struct {
msg string
a []meanScoreRank
expected []meanScoreRank
}{
{
msg: "normal case",
a: []meanScoreRank{
{1, "player0001", 10000},
{2, "player0002", 9000},
{3, "player0003", 8000},
{4, "player0004", 7000},
{5, "player0005", 6000},
{6, "player0006", 5000},
{7, "player0007", 4000},
{8, "player0008", 3000},
{9, "player0009", 2000},
{10, "player0010", 1000},
},
expected: []meanScoreRank{
{1, "player0001", 10000},
{2, "player0002", 9000},
{3, "player0003", 8000},
{4, "player0004", 7000},
{5, "player0005", 6000},
{6, "player0006", 5000},
{7, "player0007", 4000},
{8, "player0008", 3000},
{9, "player0009", 2000},
{10, "player0010", 1000},
},
},
{
msg: "empty pattern",
a: []meanScoreRank{},
expected: []meanScoreRank{},
},
{
msg: "one record pattern",
a: []meanScoreRank{
{1, "player0001", 10000},
},
expected: []meanScoreRank{
{1, "player0001", 10000},
},
},
{
msg: "more ten record case",
a: []meanScoreRank{
{1, "player0001", 10000},
{2, "player0002", 9000},
{3, "player0003", 8000},
{4, "player0004", 7000},
{5, "player0005", 6000},
{6, "player0006", 5000},
{7, "player0007", 4000},
{8, "player0008", 3000},
{9, "player0009", 2000},
{10, "player0010", 1000},
{11, "player0011", 900},
},
expected: []meanScoreRank{
{1, "player0001", 10000},
{2, "player0002", 9000},
{3, "player0003", 8000},
{4, "player0004", 7000},
{5, "player0005", 6000},
{6, "player0006", 5000},
{7, "player0007", 4000},
{8, "player0008", 3000},
{9, "player0009", 2000},
{10, "player0010", 1000},
},
},
{
msg: "more ten record case all tie score",
a: []meanScoreRank{
{1, "player0001", 10000},
{1, "player0002", 10000},
{1, "player0003", 10000},
{1, "player0004", 10000},
{1, "player0005", 10000},
{1, "player0006", 10000},
{1, "player0007", 10000},
{1, "player0008", 10000},
{1, "player0009", 10000},
{1, "player0010", 10000},
{1, "player0011", 10000},
},
expected: []meanScoreRank{
{1, "player0001", 10000},
{1, "player0002", 10000},
{1, "player0003", 10000},
{1, "player0004", 10000},
{1, "player0005", 10000},
{1, "player0006", 10000},
{1, "player0007", 10000},
{1, "player0008", 10000},
{1, "player0009", 10000},
{1, "player0010", 10000},
{1, "player0011", 10000},
},
},
{
msg: "output more ten rank case",
a: []meanScoreRank{
{1, "player0001", 10000},
{2, "player0002", 9000},
{3, "player0003", 8000},
{4, "player0004", 7000},
{5, "player0005", 6000},
{6, "player0006", 5000},
{7, "player0007", 4000},
{8, "player0008", 3000},
{9, "player0009", 2000},
{10, "player0010", 1000},
{10, "player0011", 1000},
},
expected: []meanScoreRank{
{1, "player0001", 10000},
{2, "player0002", 9000},
{3, "player0003", 8000},
{4, "player0004", 7000},
{5, "player0005", 6000},
{6, "player0006", 5000},
{7, "player0007", 4000},
{8, "player0008", 3000},
{9, "player0009", 2000},
{10, "player0010", 1000},
{10, "player0011", 1000},
},
},
}
for k, v := range cases {
t.Run(fmt.Sprintf("#%d %s", k, v.msg), func(t *testing.T) {
got := extractTopTenScoreRank(v.a)
if !reflect.DeepEqual(got, v.expected) {
t.Errorf("\nwant:\n %v, \ngot:\n %v", v.expected, got)
}
})
}
}
func Test_convertToPlayLogs(t *testing.T) {
type args struct {
rows [][]string
}
cases := []struct {
msg string
args args
expected []*playLog
}{
{
msg: "normal pattern",
args: args{[][]string{
{"2021/01/01 12:00", "player0001", "10000"},
{"2021/01/01 12:00", "player0002", "9000"},
}},
expected: []*playLog{
{"2021/01/01 12:00", "player0001", 10000},
{"2021/01/01 12:00", "player0002", 9000},
},
},
}
for k, v := range cases {
t.Run(fmt.Sprintf("#%d %s", k, v.msg), func(t *testing.T) {
if got := convertToPlayLogs(v.args.rows); !reflect.DeepEqual(got, v.expected) {
t.Errorf("\nwant:\n %v, \ngot:\n %v", v.expected, got)
}
})
}
}
テスト実行結果
$ go test -v
=== RUN Test_readCsvBodyAndConvertToSlice
=== RUN Test_readCsvBodyAndConvertToSlice/#0_normal_pattern
--- PASS: Test_readCsvBodyAndConvertToSlice (0.00s)
--- PASS: Test_readCsvBodyAndConvertToSlice/#0_normal_pattern (0.00s)
=== RUN Test_calculateMeanScoreAndConvertToMeanScoreRank
=== RUN Test_calculateMeanScoreAndConvertToMeanScoreRank/#0_normal_pattern
=== RUN Test_calculateMeanScoreAndConvertToMeanScoreRank/#1_float_pattern
--- PASS: Test_calculateMeanScoreAndConvertToMeanScoreRank (0.00s)
--- PASS: Test_calculateMeanScoreAndConvertToMeanScoreRank/#0_normal_pattern (0.00s)
--- PASS: Test_calculateMeanScoreAndConvertToMeanScoreRank/#1_float_pattern (0.00s)
=== RUN Test_sortOrderByMeanScoreAndRank
=== RUN Test_sortOrderByMeanScoreAndRank/#0_normal_pattern
=== RUN Test_sortOrderByMeanScoreAndRank/#1_same_rank_pattern
=== RUN Test_sortOrderByMeanScoreAndRank/#2_random_pattern
=== RUN Test_sortOrderByMeanScoreAndRank/#3_more_ten_record_pattern
--- PASS: Test_sortOrderByMeanScoreAndRank (0.00s)
--- PASS: Test_sortOrderByMeanScoreAndRank/#0_normal_pattern (0.00s)
--- PASS: Test_sortOrderByMeanScoreAndRank/#1_same_rank_pattern (0.00s)
--- PASS: Test_sortOrderByMeanScoreAndRank/#2_random_pattern (0.00s)
--- PASS: Test_sortOrderByMeanScoreAndRank/#3_more_ten_record_pattern (0.00s)
=== RUN Test_extractTopTenScoreRank
=== RUN Test_extractTopTenScoreRank/#0_normal_case
=== RUN Test_extractTopTenScoreRank/#1_empty_pattern
=== RUN Test_extractTopTenScoreRank/#2_one_record_pattern
=== RUN Test_extractTopTenScoreRank/#3_more_ten_record_case
=== RUN Test_extractTopTenScoreRank/#4_more_ten_record_case_all_tie_score
=== RUN Test_extractTopTenScoreRank/#5_output_more_ten_rank_case
--- PASS: Test_extractTopTenScoreRank (0.00s)
--- PASS: Test_extractTopTenScoreRank/#0_normal_case (0.00s)
--- PASS: Test_extractTopTenScoreRank/#1_empty_pattern (0.00s)
--- PASS: Test_extractTopTenScoreRank/#2_one_record_pattern (0.00s)
--- PASS: Test_extractTopTenScoreRank/#3_more_ten_record_case (0.00s)
--- PASS: Test_extractTopTenScoreRank/#4_more_ten_record_case_all_tie_score (0.00s)
--- PASS: Test_extractTopTenScoreRank/#5_output_more_ten_rank_case (0.00s)
=== RUN Test_convertToPlayLogs
=== RUN Test_convertToPlayLogs/#0_normal_pattern
--- PASS: Test_convertToPlayLogs (0.00s)
--- PASS: Test_convertToPlayLogs/#0_normal_pattern (0.00s)
PASS
ok ./yumemi_cording_test 0.098s
ディレクトリ構成
yumemi_cording_test
├── README.md
├── csv_generator
│ ├── README.md
│ ├── main.go
│ └── result
│ └── game_score_202209102336.csv
├── go.mod
├── input_data
│ └── game_score.csv
├── main.go
├── main_test.go
├── result
│ └── get_ranking game_score_log_2022-09-10T23:39:40+09:00.csv
└── test_data
└── game_score_test.csv
まとめ
いろいろと調べながらですが、なんとか動いてくれてよかったです。
ただ、制限時間200分にはとても間に合わなかったので、スピードも今後意識していけたらなと思います。
処理を実装するための手法を調べる時間や、アルゴリズムについて調べる時間が結構かかってしまったので、
そこにもうちょっと詳しくなることと、関数名や変数名を付ける際に、類語をある程度知っておくこともスピードに影響してくるかと思いました。
あと一度実装してから、リーダブルコードを読んだら、コメントを減らしてより関数名に意味を込めた方がいいなとか、こう思ったとか改善点とかはコメントに残しておこうなど、修正が捗ったので、もうちょっと読んでかっこいいコードにできたらと思います。
おまけ
入力に使用するCSVファイルを手作業で作るのも時間かかるので、CSV生成ツールも作成しておきました。
以下実装です。(テストは書いてないです。)go run main.go
で実行し、行数を入力するとCSVファイルが生成される簡単なものです。
package main
import (
"bufio"
"encoding/csv"
"fmt"
"log"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
func main() {
stringInput := strStdin()
n, err := strconv.Atoi(stringInput)
if err != nil {
log.Fatalf("cannot convert to int: %v\n", err)
}
rows := generateCsvRows(n)
timestamp := time.Now().Format("200601021504")
file, err := os.Create("./result/game_score_" + timestamp + ".csv")
if err != nil {
log.Fatalln(err)
}
w := csv.NewWriter(file)
if err := w.WriteAll(rows); err != nil {
log.Fatalln("error writing record to csv:", err)
}
}
func strStdin() (stringInput string) {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
stringInput = scanner.Text()
stringInput = strings.TrimSpace(stringInput)
return
}
func generateCsvRows(n int) [][]string {
rows := [][]string{}
header := []string{"create_timestamp", "player_id", "score"}
rows = append(rows, header)
for i := 0; i < n; i++ {
timestamp := time.Now().Format("2006/01/02 15:04")
rand.Seed(time.Now().UnixNano())
id := fmt.Sprintf("%04d", rand.Intn(9999))
playerId := "player" + id
rand.Seed(time.Now().UnixNano())
score := strconv.Itoa(rand.Intn(99999))
rows = append(rows, []string{timestamp, playerId, score})
}
return rows
}