株式会社ゆめみ さんのコーディング試験模試をRustで解いてみた

# はじめに

[株式会社ゆめみ | YUMEMI ](https://www.yumemi.co.jp/)さんがホームページで公開されている、[*サーバーサイドエンジニア応募者向けの模試*](https://www.yumemi.co.jp/serverside_recruit) を、最近勉強を始めた [**Rust**](https://www.rust-lang.org/) で解いてみました。
今回は、その解法に加えて、意識した点を記していきます。尚、この記事を書いているのは実務未経験、プログラミング学習歴1年にも満たない超ド級ペーペーの学生だという点はご了承ください。

# コーディング試験 問題例

https://www.yumemi.co.jp/serverside_recruit

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

概要については引用の通り、CLIプログラムを開発するというものです。
その他、評価観点や入出力ルールなどの詳細については引用元をご覧ください。

# 解き始める前に考えること
解法に移る前に私が意識していることについてのお話。
私はときどき勉強の息抜きで[paizaラーニング](https://paiza.jp/works)というサービスのプログラミングの問題を解くことがるのですが、そういうときに大事にしているということと言うか、まず考えることがあります。
それは、問題を読んでいきなり手を動かしてコードを書き始めようとはせず、自分がその問題を抱えている*人*や*モノ*、またはその*プログラム自体*になりきることです！ ~~(意味わからん)~~ 感覚の話ですみません。

例えば今回の問題だったら、ゲームのプレイログからランキングを作成したいわけなので、こんな感じで考えます。

- まずはCLIからCSVファイルのパスをもらって〜🤔💭
- それを読んでランキング表作らなかんな〜🤔💭
    - そのためにログ全部読んで各プレイヤーごとにスコア平均とらなかん -> 連想配列や💡(確信)
    - この配列をもとにランクできそう🤔💭
- あとはそれをCSV形式にシリアライズして標準出力に表示させるだけや💪🏻🔥

みたいな感じでした、たぶん。
私には問題読んだだけで直接コーディングを開始できるような能力はまだないので、こんな感じで手順を整理しながら、やるべきことや必要なものを考えます。そして、これをコードに落とし込んでいきます。

# 解法

<details><summary> ソースの全貌はこのトグルの中。 </summary>

```rust: src/mani.rs
use std::{
    collections::{BTreeMap, HashMap},
    path::Path,
};

use anyhow::Result;
use clap::Parser;
use serde::{Deserialize, Serialize};

const LIMIT: u64 = 10;

#[derive(Parser)]
#[clap(version, about, long_about = None)]
struct Arg {
    /// Path to CSV file
    csv_file_path: String,
}

#[derive(Debug, Deserialize)]
struct LogValue {
    // create_timestamp: String, // unused
    player_id: String,
    score: u64,
}

#[derive(Debug, Serialize)]
struct RankingValue<'a> {
    rank: u64,
    player_id: &'a str,
    mean_score: u64,
}

struct ScoreData {
    sum: u64,
    count: u64,
}

fn main() -> Result<()> {
    let arg = Arg::parse();

    let path = Path::new(&arg.csv_file_path);
    let mut reader = csv::Reader::from_path(path)?;

    // set player_data_map
    let mut player_data_map = HashMap::new();
    reader.deserialize().for_each(|result| {
        let record: LogValue = result.unwrap();
        let score_data = player_data_map
            .entry(record.player_id)
            .or_insert(ScoreData { sum: 0, count: 0 });
        score_data.sum += record.score;
        score_data.count += 1;
    });

    // set mean_score_map
    let mut mean_score_map = BTreeMap::new();
    player_data_map.iter().for_each(|(player_id, score_data)| {
        let mean_score = (score_data.sum as f64 / score_data.count as f64).round() as u64;
        let player_id_vec = mean_score_map.entry(mean_score).or_insert(vec![]);
        player_id_vec.push(player_id.clone());
    });

    // create ranking in CSV format
    let mut writer = csv::Writer::from_writer(vec![]);
    let mut count = 1;
    let mut rank = 1;

    mean_score_map
        .iter()
        .rev()
        .take_while(move |_| count < LIMIT)
        .for_each(|(mean_score, player_id_vec)| {
            player_id_vec.iter().for_each(|player_id| {
                writer
                    .serialize(RankingValue {
                        rank,
                        player_id,
                        mean_score: *mean_score,
                    })
                    .unwrap();
                count += 1;
            });
            rank += player_id_vec.len() as u64;
        });

    println!("{}", String::from_utf8(writer.into_inner()?)?);

    Ok(())
}
```

</details>

## 1.依存関係を追加

```toml: Cargo.toml
[package]
name = "get_ranking"
version = "0.1.0"
edition = "2021"
description = "Reads CSV-formatted play logs of three specific columns and calculates the top 10 ranking players"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0.56"
csv = "1.1"
clap = { version = "3.1.6", features = ["derive"] }
serde = { version = "1.0.136", features = ["derive"] }
```

- `clap` : CLIアプリケーションを開発するということなので以前触れたことがあるこちらを選定
- `csv` , `serde` : CSVファイルを読み込み、シリアライズ/デシリアライズするため

## 2. 引数を受け取り、CSVパーサーを作成

```rust
use std::path::Path;

use anyhow::Result;
use clap::Parser;

#[derive(Parser)]
#[clap(version, about, long_about = None)]
struct Arg {
    /// Path to CSV file
    csv_file_path: String,
}

fn main() -> Result<()> {
    let arg = Arg::parse();

    let path = Path::new(&arg.csv_file_path);
    let mut reader = csv::Reader::from_path(path)?;

    Ok(())
}
```

`csv::Reader::from_path`は内部的に`io::BufReader`でファイルを読むから高速。

`clap`は`#[clap(...)]`のような手続き型マクロをや、`///` でドキュメンテーションコメントをフィールドの頭に付与するだけで自動的にヘルプドキュメントを生成してくれるから便利です。

こんな感じ。

```console
get_ranking 0.1.0
Reads CSV-formatted play logs of three specific columns and calculates the top 10 ranking players

USAGE:
    get_ranking <CSV_FILE_PATH>

ARGS:
    <CSV_FILE_PATH>    Path to CSV file

OPTIONS:
    -h, --help       Print help information
    -V, --version    Print version information
```

## 3. デシリアライズ -> HashMapを作成(key: player_id, value: ScoreData)

```rust
use std::{
    collections::{BTreeMap, HashMap},
    // [...]
};

// [...]
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct LogValue {
    // create_timestamp: String, // unused
    player_id: String,
    score: u64,
}

struct ScoreData {
    sum: u64,
    count: u64,
}

fn main() -> Result<()> {
    // [...]

    // set player_data_map
    let mut player_data_map = HashMap::new(); // [key:player_id, value:ScoreData{sum, count}]
    reader.deserialize().for_each(|result| {
        let record: LogValue = result.unwrap();
        let score_data = player_data_map
            .entry(record.player_id)
            .or_insert(ScoreData { sum: 0, count: 0 });
        score_data.sum += record.score;
        score_data.count += 1;
    });

    Ok(())
}
```  

## 4. ランキングを作ってCSV形式にシリアライズ -> 標準出力に表示

```rust
// [...]

use serde::{Deserialize, Serialize};

const LIMIT: u64 = 10;

// [...]

#[derive(Debug, Serialize)]
struct RankingValue<'a> {
    rank: u64,
    player_id: &'a str,
    mean_score: u64,
}

fn main() -> Result<()> {
    // [...]

    // set mean_score_map
    let mut mean_score_map = BTreeMap::new();
    player_data_map.iter().for_each(|(player_id, score_data)| {
        let mean_score = (score_data.sum as f64 / score_data.count as f64).round() as u64;
        let player_id_vec = mean_score_map.entry(mean_score).or_insert(vec![]);
        player_id_vec.push(player_id.clone());
    });

    // create ranking in CSV format
    let mut writer = csv::Writer::from_writer(vec![]);
    let mut count = 1;
    let mut rank = 1;
    mean_score_map
        .iter()
        .rev()
        .take_while(move |_| count < LIMIT)
        .for_each(|(mean_score, player_id_vec)| {
            player_id_vec.iter().for_each(|player_id| {
                writer
                    .serialize(RankingValue {
                        rank,
                        player_id,
                        mean_score: *mean_score,
                    })
                    .unwrap();
                count += 1;
            });
            rank += player_id_vec.len() as u64;
        });

    println!("{}", String::from_utf8(writer.into_inner()?)?);

    Ok(())
}
```

わざわざ構造体を定義してシリアライズしなくても`for`の中で`println!`してベタ書きすることもできますが、なんとなく汚いし、あまりよろしくない気がしたので`csv::Writer`を使って丁寧に書きました。


# 解いてみた所感

最初に評価観点を読んでみて、求められているものとしては、

- 誰が見ても何をしているのかわかるように`適切にコメントを書き残すこと`
- 将来的にどんな仕様変更や機能追加がされるかを見越して、`それを容易に反映できるように書くこと`

などがあるのかなと私なりに解釈しました。
ですが後者が難しくて...。

仕様変更や機能追加と言ったら、CSV以外の他のフォーマットにも対応、日付別にランク付け、とかがあるのかな？それらの変更にも対応しやすいようなコード設計…難しい。
とりあえず`main`関数の中で期待通りに動く程度に完成させて、その後にリファクタリングしようと思っていたのですが、考えすぎて、手が動かなくなってるうちに時間切れでした。
構造体や関数を定義してうまい感じにロジックを切り出すべきなのだとは思いますが、どうやるべきか、その辺の感覚がまだ乏し過ぎるようです。

結果的に時間内にできたものは`main`関数の中にすべてのロジックが詰め込まれているだけだからめっちゃ汚い。よろしくないですね。

# おわりに

今回はセルフで時間制限を設けた上で、ゆめみさんの模試を解いてみました。

力不足が露呈して悲しくなりましたが、自分に不足している力を知ることができたのは成長です。
これからもとにかく手を動かして、やってみて、技術も感覚も培っていこうとぞ思ひます。

最後まで読んでいただきありがとうございました。

おわり。