株式会社ゆめみサーバサイドコーディング試験模試(2023年度以降版)の回答例とフィードバック例

# はじめに

株式会社ゆめみでは、採用時にコーディング試験を課しています。
2023年5月中旬より、数年間内容を更新せずに利用してきたこのコーディング試験の内容を刷新、合わせて模試も新しいものにすることとしました。（※まずは2025年度新卒採用から変更しますが、順次展開する予定です。）
この記事では、2023年度版のサーバサイドコーディング試験の模試の内容、およびそれに回答があった場合にゆめみがどういう観点でどういうレビューを実施するかをまるっと全部解説します。
※2024年度のコーディング試験も同じ内容で実施しております。

過去のコーディング試験の目的とレビュー観点はこちらを御覧ください。

https://qiita.com/taruhachi/items/0318c1a603478058d863

## 刷新した理由
- 5年以上同じ問題を利用してきて、準備されたテストケースの通過率がほとんどの人が90%以上となってしまったこと。
- 応募者のレベルが上ってきたことに合わせて、より難易度を上げないと差別化が難しくなってきたこと。
-  **【New!!】本試験もChatGPTに回答できそうな問題となっていたこと**

## この模試および本試験の位置付け
この模試は実際の本試験と同じ環境であるTrack Test上に弊社のオリジナル問題として登録してあります。
[ゆめみサーバサイド模試2023 on TrackTest](https://app.tracks.run/yumemi/share/36e231bd9b3140d2aeae0320c35b25e4)

:::note info
Track Test（トラックテスト）は、豊富な問題を組み合わせることで、企業ごとに独自の技術課題を簡単に作成・配信できます。候補者の皆さまから提出されたソースコードを自動採点し、レビューができる、コーディングテストツールです。
:::

:::note info
今回、この模試は試験プラットフォーム提供元である、ギブリー様のご厚意およびご協力により、本番と同じ環境で全ての方にご自由に受験していただくことができることとなりました。

こちらから是非ご受験ください。
:::


この模試は、本試験と同じ環境で回答するための準備や力試しができるというだけではありません。
今回、株式会社ゆめみサーバサイドコーディング試験の本試験はこの模試と同じシチュエーションにおいて、仕様追加、仕様変更が発生したものという位置づけとなっています。
そして、 **この模試でご自身が書かれたコードをお手元に保存されて置かれた場合は、そのコードを本試験に持ち込みをしてOKです。** 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38018/0f7cd040-e697-3c9e-772e-6b993a375e21.png)



つまり、2023年度版のコーディング試験は、**仕様追加・変更に対する対応力を特に重視**した試験となっています。
模擬試験の段階におきましても、これまでの試験以上に、 **どれだけ将来の仕様追加・変更があるだろうという予測ができるか。それらに対応したコーディングができるか。** という能力も必要となっています。

---
# サーバサイドコーディング試験模試(2023年度版)の試験内容
本番試験ではこのコーディング問題に加えて、SQLでDB中のデータを操作する、SQL問題も追加された全2問での構成となります。

## 概要
プログラミング問題 1題
制限時間200分（※本試験は任意のタイミングで開始してから解答提出まで24時間の猶予をもうけています。）
指定された仕様に基づいたコマンドラインプログラムをコーディングしていただきます。
言語は以下の中から選ぶことができます。(※コードレビュー可能なエンジニアの調達状況により変動することがあります。)
C, C++, Java, JavaScript, TypeScript, PHP, Python3, Ruby, Go, Kotlin, Rust
※ Python3など、弊社の通常の開発においてはあまり使われていない言語も含まれていますが、まずは応募者の方が習熟された言語において、上記の観点を満たしたコードを記載する事ができるかどうかを観ています。

## 仕様
あなたは、あるe-sports大会で集められたゲームの`エントリーファイル`と`プレイログファイル`をもとに、ランキング上位10位までを算出することになりました。
このランキングを算出するCLIプログラムの開発をしてください。

### CLI
指定されたファイルを処理して得られたランキング入賞者を標準出力に表示するプログラムを、CLIアプリケーションとして実装してください。

### プログラム起動時のルール
+ CLIアプリケーションは2つの引数を受け取ります。
+ 第1引数は、ゲームのエントリーファイルのファイル名です。（※プログラム実行箇所からのパスを含むものとします。）
+ 第2引数は、ゲームのプレイログのファイル名です。

#### プログラム起動例
```plain
$ ./get_ranking game_entry_log.csv game_score_log.csv
```

### 入力ファイルの仕様

#### ゲームのエントリーファイルの構造
エントリーファイルは、大会にエントリーしたプレイヤーが記録されたファイルです。

+ エントリーは2列のCSV（引用符、改行なし）ファイルとして提供されます。
+ 1行目は、ヘッダーとして`player_id,handle_name`と記載されています。
+ エントリーデータは2行目以降に記録されており、1行目のヘッダーの各項目に対応したデータが記載されています。
+ `player_id`はゲームにエントリーしているプレイヤーごとに一つずつ払い出された個別のIDで、全ての行で異なる`player_id`が割り当てられていることが保証されています。
+ `handle_name`はエントリーしたユーザーのハンドルネームです。
+ ハンドルネームは異なるプレイヤーであっても同じ名前を利用することができます。
+ エントリーするプレイヤーの総数は1万人を超えることはありませんが、1万人を超えてもエラーとして処理をする必要はありません。

#### ゲームのプレイログファイルの構造
プレイログファイルは、プレイヤーがゲームをプレイしたタイミングで追記されたログファイルです。
このログファイルには、大会にエントリーしていないプレイヤーのプレイログも含まれています。

+ プレイログは3列のCSV（引用符、改行なし）ファイルとして提供されます。
+ 1行目は、ヘッダーとして`create_timestamp,player_id,score`と記載されています
+ プレイログは2行目以降に記録されており、1行目のヘッダーの各項目に対応したデータが記載されています。
+ `create_timestamp`はプレイヤーがこのゲームをプレイした日時を示しています。
+ このログは`create_timestamp`でソートされています。
+ `player_id`はゲームエントリーファイルで定義されるIDと同じものですが、大会にエントリーしていないプレイヤーにも一意に発行されています。
+ 同一のプレイヤーが複数回のプレイを実施したときには、複数行のログが記録されます。
+ 対象のプレイログ全体は数千万行以上に肥大化することがあります。


#### CSVファイル中の各要素のフォーマットについて
ゲームのプレイログファイルの各要素として以下の仕様で指定されたデータであることが予め担保されたデータとなっており、Validation不要で利用できます。

+ `create_timestamp` は `yyyy-MM-dd HH:ii:ss` 形式の文字列です
+ `player_id`はアルファベットの大文字、小文字、アンダースコア("_")、および数字の0-9のみで構成された1文字以上、20文字以下の文字列となります。
+ `handle_name`はアルファベットの大文字、小文字、アンダースコア("_")、および数字の0-9のみで構成された1文字以上、20文字以下の文字列となります。
+ `score`はゲームで獲得した得点で、0以上の整数となります。


### ランキング抽出ルール
+ エントリーデータおよび、プレイデータをもとに、プレイヤー毎の最高得点によるランキングを作成してください。
+ 大会にエントリーしていないプレイヤーのプレイデータは集計の対象としません。
+ プレイヤー毎の最高得点を後述する`score`の項目に出力してください。

### 正常処理時の出力ルール
+ 下記のデータを標準出力に出力してください。
+ 出力は4列のCSV形式としてください。（クオートは不要）
+ 1行目はヘッダーとして、`rank,player_id,handle_name,score`を出力してください。
+ 上記ヘッダーに準じて2列目以降を出力してください。
+ `rank`の項目には最高得点の上位から1,2,3,…の数字を割り当ててください。
+ `score`が同点のプレイヤーが居た場合、`rank`の数字は同じ数字を割り当ててください。
+ `score`上位10名のプレイヤーが含まれるまでのランキングを出力してください。
+ `score`が同点のプレイヤーが居た場合、10名以上がランキングに含まれることがあります。
+ 同一ランクのプレイヤーが居た場合、`player_id`でソートしてください。
+ 改行コードは[LF]としてください。

#### 正常時入出力例:
```plain
$ ./get_ranking game_entry_log.csv game_score_log.csv
rank,player_id,handle_name,score
1,player0001,HANDLE_NAME_1,10000
1,player0002,HANDLE_NAME_2,10000
3,player0003,HANDLE_NAME_3,9000
4,player0004,HANDLE_NAME_4,7000
5,player0005,HANDLE_NAME_5,1000
5,player0006,HANDLE_NAME_6,1000
7,player0007,HANDLE_NAME_7,998
8,player0008,HANDLE_NAME_8,997
9,player0009,HANDLE_NAME_9,990
9,player0010,HANDLE_NAME_10,990
9,player0011,HANDLE_NAME_11,990
9,player0012,HANDLE_NAME_12,990
```

### 異常系出力ルール
+ 指定されたログファイルが存在しない場合や、ログファイルの内容が仕様と異なった場合などは、異常系として処理を中断してください。
+ 異常系におけるメッセージは任意のメッセージを出力可能ですが、それらのメッセージは標準エラー出力として出力してください。
+ 上記の場合において、標準出力には何も出力しないでください。
+ 上記の場合において、プログラムを 0 以外の終了コードで終了させてください。
+ 集計した結果、ランキングに入賞するレコードがなかった場合は異常系として扱わず、正常系として1行目のヘッダー行のみ出力してください。


#### 異常時入出力例:
下記の出力は標準エラー出力に出力されます。
```plain
$ ./get_ranking highscore
入力引数の数が不正です。
```

---
# 模擬試験への回答例(PHP)
こちらの模擬試験に対する、回答例はChatGPTさん(GPT-3.5)さんに作成頂きました。
※今回の記事の本質ではないので、ChatGPTに指定したスクリプト内容などの詳細はこの記事では省略します。興味のある方はこちらで解説していますので、ご覧ください。
https://qiita.com/taruhachi/private/e88e68987f7dfd7b0264

回答内容はこちらです。

``` php
<?php
function getEntryData(string $entryFileName): array {
    $entryData = array();
    $entryFile = fopen($entryFileName, "r");
    // header行を読み飛ばす
    fgetcsv($entryFile);
    while (($data = fgetcsv($entryFile)) !== FALSE) {
        $entryData[$data[0]] = $data[1];
    }
    fclose($entryFile);
    return $entryData;
}

function getPlayLogData(string $playLogFileName, array $entryData): array {
    $playLogData = array();
    $playLogFile = fopen($playLogFileName, "r");
    while (($data = fgetcsv($playLogFile)) !== FALSE) {
        if (array_key_exists($data[1], $entryData)) {
            if (!isset($playLogData[$data[1]]) || $playLogData[$data[1]] < $data[2]) {
                $playLogData[$data[1]] = $data[2];
            }
        }
    }
    fclose($playLogFile);
    return $playLogData;
}

function sortPlayLogData(array $playLogData): array {
    ksort($playLogData);
    arsort($playLogData);
    return $playLogData;
}


function extractRankingData(array $playLogData, array $entryData): array {
    $rankingData = array();
    $rank = 0;
    $prevScore = 0;
    foreach ($playLogData as $playerId => $score) {
        if (array_key_exists($playerId, $entryData)) {
            $rank++;
            if ($score !== $prevScore) {
               $printRank = $rank;
            }
            if ($printRank > 10) {
                break;
            }
            $rankingData[$rank] = array($printRank,$playerId, $entryData[$playerId], $score);
            $prevScore = $score;
        } else {
            // If $playerId does not exist in $entryData, skip it.
            continue;
        }
    }
    return $rankingData;
}

function outputRankingData(array $rankingData): void {
    echo "rank,player_id,handle_name,score\n";
    foreach ($rankingData as $rank => $data) {
        echo "$data[0],$data[1],$data[2],$data[3]\n";
    }
}


function main(string $entryFileName, string $playLogFileName): void {

    // Check if the entry file and play log file exist
    if (!file_exists($entryFileName) || !file_exists($playLogFileName)) {
        fwrite(STDERR, "Error: File does not exist.\n");
        exit(1);
    }


    // Read the entry file into an array
    $entryData = getEntryData($entryFileName);

    // Read the play log file into an array
    $playLogData = getPlayLogData($playLogFileName, $entryData);

    // Sort the play log data by score in descending order
    $playLogData = sortPlayLogData($playLogData);

    // extract ranking data
    $rankingData = extractRankingData($playLogData, $entryData);

    // Output the ranking data
    outputRankingData($rankingData);
}


// Check if the number of command line arguments is correct
if ($argc != 3) {
    fwrite(STDERR, "Error: Invalid number of arguments.\n");
    exit(1);
}

// Get the entry file and play log file names from command line arguments
$entryFileName = $argv[1];
$playLogFileName = $argv[2];

main($entryFileName, $playLogFileName);
```

---
# 模擬試験解答例へのコードレビュー例

ゆめみでは、本試験においてはコードレビュー結果を応募者に対してフィードバックを返信しています。
今回、このChatGPTさんの解答に対してどういうフィードバックを実施するかの例を記載します。

ちなみに、フィードバックは、レビュワーが異なっても粒度が揃うように、テンプレートに照らし合わせることである程度やりやすくなるようにしています。

まずはテンプレートと照らし合わせてみます。
**※ 本試験に繋がる評価項目のため、一部の項目はここでは記載していません。**

## 評価テンプレートとの突き合わせ
| 観点                   | 項目                       | 評価   | 
|----------------------|--------------------------|------|
| テストの通過率              | 正常系の公開テストの通過率            | 100% |
| 異常系やValidationエラーの扱い | 公開テスト（引数の数チェック）          | OK   | 
| 異常系やValidationエラーの扱い | 公開テスト（ファイル存在チェック）        | OK   | 
| 全体的に可読性が高いコードか       | main()関数が簡潔で見通しが良いか      | OK   | 
| 全体的に可読性が高いコードか       | 適切な量のコメントが残されているか        | OK   | 
| 全体的に可読性が高いコードか       | 関数名や変数名が適切か              | OK   | 
| 全体的に可読性が高いコードか       | 適切な変数を利用できているか           | 一部NG | 
| 問題の分解について            | 入力→処理→出力のブロック化ができているか    | OK   |  
| 問題の分解について            | 入力→処理→出力がそれぞれ関数化できているか   | OK   |
| 問題の分解について            | 関数の責務として、複数の責務が混ざっていないか  | OK   | 
| 型の利用について             | 型を意識したコードか               | OK   | 
| 定数や設定値の扱いが適切か        | 仕様書中の設定値を定数的に扱うことができているか | NG   | 
| 定数や設定値の扱いが適切か        | 上記の数字は仕様書中にあらわれているか      | NG   |
| その他 | その他気になる部分があるか | |

## 応募者へのフィードバック例
上記を踏まえて、もし今回のChatGPTさんの解答へフィードバックを実施するとするとその例は以下となります。 
このフィードバックは、プログラムとして準備されたテストケースではありませんので、例えテストケースの通過率が100%だったとしても発生するレビューとなります。
通常、弊社では **＜評価ポイント＞** と **＜改善ポイント＞** の2つをセットで伝えることとしています。

**<評価ポイント>**
- 準備されたテストに関して全て通過させています。（**※100%になるまで、手動で修正を繰り返しただけであることに注意** ）
- 起動時のパラメータ数が不正な場合にきちんと異常系での停止ができておりました。
- 指定されたファイルが存在しなかったときに、きちんと異常系での停止ができておりました。
- main()処理部分が非常に処理を追いやすい簡潔なものとして記載されています。
- 過不足なく適切なレベルでのコメントが残されております。
- 適切な関数名や変数名を利用する事ができています。
- プログラムの流れが、入力→処理→出力となっていることで処理を追いやすくなっています。
- プログラムの、入力、処理、出力をそれぞれ別の責務の関数に渡すことができています。
- 関数の責務が混ざっておらず、リファクタリングがやりやすくなっています。
- TypeHintingを利用した型を意識したコードが書けています。
- ~~本試験時の評価項目となるので割愛…~~
- ~~本試験時の評価項目となるので割愛…~~

**<改善ポイント>**
- 一部、`$data[0]`や`$data[1]`など、配列の中身を直接参照している箇所がありますが、それらのデータは一度`$player_id`や`$score`などの変数に入れた方が可読性がより高くなるでしょう。
- 問題文中で指定された文字列や数字はコード中のマジックナンバーとして扱うのではなく、定数的に扱ったほうがより良いです。
- 上記の定数は、問題文中にある数字をそのまま利用すると、仕様変更により強くなります。
- ~~本試験時の評価項目となるので割愛…~~
- ~~本試験時の評価項目となるので割愛…~~


# 最後に
今回、ChatGPTさんにはPHPを利用して解答していただきましたが、自由な言語で、このような形で解答していただいた後に、そのコードを本試験に持ち込む事もできます。
模擬試験に関しましては今回のようにChatGPTを利用した解答をしていただいても大丈夫ですし、力試しとして手動でのチャレンジをしていただいても大丈夫です。

:::note info
※ご自身がChatGPTを利用した作成された模擬試験の解答コードを本試験へ持ち込むことはOKですが、本試験の受験におけるChatGPTの利用は禁止させていただきます。
:::

また、本試験と同じ環境で受験できる、ギブリー社様提供のTrackTest上の模試では、仕様を満たすかどうかをチェックするテストケースも複数準備されており、それらはWeb上で実行することも可能となっていますので、まずは模試から、触ってみて頂ければ幸いです。

:::note info
※ TrackTestのシステムにおきましては、問題文の外部流出を防ぐため、問題文のコピペができないシステムとなっています。ChatGPTに模試の仕様を与える際には本ページからコピペをする必要があります。
:::

# 関連ページ
+ [株式会社ゆめみ採用ページ](https://www.yumemi.co.jp/category/recruit)
+ [TrackTest](https://tracks.run/lp/tracktest-no1/) ※今回の模試を受験することができるシステム提供にご協力いただいたギブリー社のサイトに飛びます。
+ [株式会社ゆめみサーバサイドコーディング試験模試をChatGPTで解答してみる](https://qiita.com/taruhachi/private/e88e68987f7dfd7b0264)
+ [ゆめみサーバサイド模試2023 on TrackTest](https://app.tracks.run/yumemi/share/36e231bd9b3140d2aeae0320c35b25e4)