1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ゆめみ株式会社の採用試験をGoで解いてみた

Last updated at Posted at 2022-09-11

Goについて勉強中なのですが、自分はこれまであまり実装を行ったことがなく、チームビルディングやビジネス寄りを担当していたことが多いため、とにかくなんでもいいから書いてみようと思い、ゆめみ株式会社のサーバーサイド採用試験サンプルで練習してみました。
解いていく中で、ここもっと改善できそうだなとか、ここの仕様は次にコード見た時忘れちゃってそうだなというところはコメントに残して、あとはそんなにコメントしないように意識しました。
まだまだ他の人の解法記事などを読んでいると、自分のコードはもっさりした関数になっていてかっこよくないなと思ってしまいます。
すこしでもかっこいいコードが書けるようにもっと場数踏まなきゃと思います。

問題から何が必要かを考えるフェーズ

まず、問題文を見て、どういった処理が必要かを洗い出しました。

  1. CSVファイルの読み込み
  2. 各プレイヤーの平均スコアを算出
  3. 平均スコアの降順に並び替え&順位づけ
  4. 10位までのプレーヤーを抽出
  5. 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)
	}
}

全実装

main.go
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)
	}
}

単体テスト

長くなってしまって恐縮なんですが、一応単体テストも書いておきます。
足りない部分もありますが、実装で確認したかったところだけ記述しました。

main_test.go

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ファイルが生成される簡単なものです。

main.go
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
}


1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?