かべブログ

TypeScript, React, Goなどでまったり個人開発。たまに競プロ。

The Rust Programming Language の章末問題を解く(8章)

はじめに

こんにちは、かべです。前回に引き続いて8章の練習問題を解いていきます。

問題はこちらにあります。

環境

以下の環境で動作させています。

  • Ubuntu 20.04.1 LTS
  • rustc 1.61.0 (fe5b13d68 2022-05-18)
  • cargo 1.61.0 (a028ae4 2022-04-29)

平均値、中央値、最頻値の計算

ベクタで保存された整数値に対して、その平均値、中央値、最頻値を求めます。

まずは平均値です。ベクタの値は整数ですが、平均値は整数にならない可能性があるため as を使って f32 に変換しています。今回実装する関数全てに言えることですが、ベクタの所有権が関数に移ってしまわないように参照で渡すことも忘れずに。

fn mean(v: &Vec<i32>) -> f32 {
    let mut m = 0.0;
    for a in v {
        m += *a as f32;
    }

    m/(v.len() as f32)
}

次に中央値です。sort で並べ替えてから中央値になる値を返します。ベクタの中身が変わってしまいますが、今回は他の計算に影響を与えないので配列のコピーは行っていません。今回は簡単のため要素数を奇数にしていますが、要素数が偶数の時も同様に書くことが出来ます。

fn median(v: &mut Vec<i32>) -> i32 {
    v.sort();

    v[v.len()/2]
}

最後に最頻値です。HashMap を使って各数が出てきた回数を数え、出てきた回数が最大となる数の集合をベクタで返しています。max_num で最頻値の登場回数を保持しており、最後に登場回数がそれと等しいもの全てを返り値のベクタに追加しています。

fn mode(v: &Vec<i32>) -> Vec<i32> {
    let mut map = HashMap::new();
    let mut max_num = 0;
    // 各値の登場回数を数える
    for a in v {
        let count = map.entry(*a).or_insert(0);
        *count += 1;
        if max_num < *count {
            max_num = *count;
        }
    }

    // 最頻値となるもの全てをベクタに入れる
    let mut modes: Vec<i32> = Vec::new();
    for (key, value) in &map {
        if *value == max_num {
            modes.push(*key);
        }
    }

    modes
}

以上を実行してみると、次のようになります。

fn main() {
    let mut v = vec![1, 1, 1, 2, 3, 4, 5, 5, 5, 6, 7];

    println!("The mean of v is {}", mean(&v));
    println!("The median of v is {}", median(&mut v));
    println!("The mode of v is {:?}", mode(&v));
}
$ cargo run
The mean of v is 3.6363637
The median of v is 4
The mode of v is [5, 1]

Big Latin への変換

英単語を Big Latin と呼ばれる規則で変換します。この規則に関して問題文から引用すると、

各単語の最初の子音は、 単語の終端に移り、"ay"が足されます。従って、"first"は"irst-fay"になります。ただし、 母音で始まる単語には、お尻に"hay"が付け足されます("apple"は"apple-hay"になります)。 となります。これに従って変換を施すだけです。

今回実装する関数でやりたいことは、

  • 一文字目が a, e, i, o, u でなければ最初の文字が単語の終端にハイフンと共に移り、語尾に ay が付く
  • 一文字目が母音ならば語尾に -hay が付く

これを素直に実装していきます。母音全てが入ったベクタを用意し、一文字目とそれぞれ比べることで変換規則を決めます。

fn biglitin(s: &String) -> String {
    let vowels = vec!["a", "e", "i", "o", "u"];

    for c in vowels {
        if &s[0..1] == c {
            return format!("{}-hay", s);
        }
    }

    return format!("{}-{}ay", &s[1..], &s[0..1]);
}

これを main から呼び出すと次のようになります。

fn main() {
    let s1 = String::from("first");
    let s2 = String::from("apple");
    
    println!("{} -> {}", s1, biglitin(&s1));
    println!("{} -> {}", s2, biglitin(&s2));
}
$ cargo run
first -> irst-fay
apple -> apple-hay

従業員管理のテキストインタフェース

HashMap とベクタで部署と雇用者の関係を管理するためのインターフェースを作ります。今回は

  • 部署への人の追加
  • ある部署にいる人の一覧を見る
  • 各部署にいる人をそれぞれアルファベット順で見る

の3つを実装します。エラー処理などをどこまでやるかが悩ましいところですが、今回はかなり妥協して

  • add {名前} to {部署} で追加(to のチェック省略)
  • list {部署} である部署の一覧取得
  • list all で社員全員の情報を取得
  • end でアプリ終了

という手抜き構成で書きます。

まず入力の取得ですが、7章で学んだ enum を使ってコマンドを良い感じにその中に落とし込んでいきます。今回は Message という名前を使って以下のように定義します。

enum Message {
    Add { department: String, name: String },
    List { department: String },
    End,
    Error,
}

Add が追加、List が一覧取得、End で終了、Error は不正なメッセージをそれぞれ表しています。かなり力技ですが入力をこの enum に分解する関数も作ります。AddList のフィールドを &str にして全体をそれに合わせてあげればもう少しすっきりしそうですが今回は以下の実装でいきます。

fn parse_command(command: Vec<&str>) -> Message {
    match command.get(0) {
        Some(method) => {
            match *method {
                "add" => {
                    match command.get(1) {
                        Some(name) => {
                            match command.get(3) {
                                Some(department) => {
                                    return Message::Add { department: department.to_string(), name: name.to_string() };
                                },
                                None => {
                                    return Message::Error;
                                }
                            }
                        },
                        None => {
                            return Message::Error;
                        }
                    }
                },
                "list" => {
                    match command.get(1) {
                        Some(department) => {
                            return Message::List { department: department.to_string() };
                        },
                        None => {
                            return Message::Error;
                        }
                    }
                },
                "end" => {
                    return Message::End;
                }
                _ => {
                    return Message::Error;
                }
            }
        },
        None => {
            return Message::Error;
        },
    }
}

次に HashMap を操作する関数を実装します。社員の追加、ある部署の社員一覧取得、全ての部署の社員一覧取得を行う関数をそれぞれ add_employee, list_department_employees, list_all_employees で実装します。8.3 節にあるように、「始めてアクセスするキーの場合はデフォルト値(ここでは空のベクタ)を挿入してその値を返し、そうでなければキーに対応する値を返す」という動作を entry(key).or_insert(default) で実現します。後は返された値に対して所望の操作を行うだけです。

fn add_employee(map: &mut HashMap<String, Vec<String>>, department: String, name: String) {
    println!("Added {} to {}.", name, department);
    let names = map.entry(department).or_insert(Vec::new());
    names.push(name);
}

fn list_department_employees(map: &mut HashMap<String, Vec<String>>, department: String) {
    println!("{}:", department);
    let names = map.entry(department).or_insert(Vec::new());
    for name in names {
        println!("{}", name);
    }
}

fn list_all_employees(map: &mut HashMap<String, Vec<String>>) {
    for (department, names) in map {
        println!("{}:", department);
        names.sort();
        for name in names {
            println!("{}", name);
        }
    }
}

最後に、ここまで実装した内容を main 関数から呼び出します。コマンドを enum で定義しているので、match で簡単に挙動を分岐させることが出来ます。コマンドのスペースでの分割は String.split().collect() を使って行います。

fn main() {
    let mut employee: HashMap<String, Vec<String>> = HashMap::new();

    println!("Usage:");
    println!("1. add (name) to (department)");
    println!("2. list all");
    println!("3. list (department)");
    println!("4. end");

    loop {
        println!("Please type your request.");

        let mut command = String::new();
        io::stdin()
            .read_line(&mut command)
            .expect("Failed to read line.");

        let command = command.trim().split(' ').collect();
        let message = parse_command(command);

        match message {
            Message::Add { department, name } => add_employee(&mut employee, department, name),
            Message::List { department } => {
                match department.as_str() {
                    "all" => list_all_employees(&mut employee),
                    _ => list_department_employees(&mut employee, department),
                }
            }
            Message::End => break,
            Message::Error => println!("Invalid request."),
        }
    }
}

簡単な機能を試した様子を以下に示します。

$ cargo run
Usage:
1. add (name) to (department)
2. list all
3. list (department)
4. end
Please type your request.
add Sally to Engineering
Added Sally to Engineering.

Please type your request.
add Amir to Sales
Added Amir to Sales.

Please type your request.
add John to Sales
Added John to Sales.

Please type your request.
list Sales
Sales:
Amir
John

Please type your request.
list all
Engineering:
Sally
Sales:
Amir
John

Please type your request.
end

終わりに

前回に比べて複雑な実装が増えてきました。さらに Rust を学ぶことでより良い実装が出来る気がするので、これからも勉強を続けていきます。