https://doc.rust-lang.org/book/ch08-03-hash-maps.html
Using a hash map and vectors, create a text interface to allow a user to add employee names to a department in a company. For example, “Add Sally to Engineering” or “Add Amir to Sales.” Then let the user retrieve a list of all people in a department or all people in the company by department, sorted alphabetically.
This language, its book and the community are just straight up awesome. I had a quick read of other reviews just before posting mine, and I decided it'd be a good idea to run cargo fmt and cargo clippy before posting. +1 for tooling!
There's two drivers for me posting this- firstly, is the data structure I'm using ok?
HashMap<String, Vec<String>>
It feels a little... strange. Because a HashMap
is re-sizeable, String
s are re-sizeable and Vec
s are re-sizeable. Is this safe and/or efficient? Am I overthinking?
Secondly, I've attempted to handle as many error cases as I can but I wonder whether there's any that I've missed? Can't wait to get to the automated testing chapter, where I can start to maybe tackle those problems. Anyway, without further ado:
// valid commands:
// ADD name TO department, DEL name FROM department
// LIST department, LIST, QUIT
use std::{
collections::HashMap,
{io, io::Write},
};
#[derive(Debug)]
struct Command {
c_type: CommandType,
args: Option<Vec<String>>,
}
#[derive(Debug)]
enum CommandType {
Add,
List,
Del,
Quit,
}
impl CommandType {
fn new(val: String) -> Option<CommandType> {
match val.as_str() {
"ADD" => Some(CommandType::Add),
"DEL" => Some(CommandType::Del),
"LIST" => Some(CommandType::List),
"QUIT" => Some(CommandType::Quit),
_ => None,
}
}
}
fn main() {
let mut employer_table: HashMap<String, Vec<String>> = HashMap::new();
employer_table.insert(
String::from("ENGINEERING"),
vec![
String::from("Lewis"),
String::from("Ellen"),
String::from("Alan"),
],
);
employer_table.insert(
String::from("SALES"),
vec![
String::from("Jane"),
String::from("Robert"),
String::from("Penny"),
],
);
loop {
let cmd: String = match user_prompt() {
Ok(val) => val,
_ => {
println!("Bad user input. Please try again...");
continue;
}
};
let cmd: Command = match command_parser(cmd) {
Some(val) => val,
None => {
println!("Invalid command... Please type ADD, DEL, LIST or QUIT.");
continue;
}
};
// QUIT is the only command that returns false
if !command_executor(cmd, &mut employer_table) {
break;
}
}
}
fn user_prompt() -> io::Result<String> {
let mut buf = String::new();
let stdin = io::stdin();
// This is a little prompt to indicate that a terminal row is typable...
// Copied this guide on SO to get this prompt working:
// https://stackoverflow.com/a/41387232
print!("> ");
io::stdout().flush().unwrap();
stdin.read_line(&mut buf)?;
Ok(String::from(buf.trim())) // Remove trailing CRLF, convert str to String
}
// valid commands:
// ADD name TO department, DEL name FROM department
// LIST department, LIST, QUIT
fn command_parser(c: String) -> Option<Command> {
let args: Vec<&str> = c.split(' ').collect(); // Convert string to array of words
let arg0 = args[0].to_uppercase();
match CommandType::new(arg0) {
Some(c_type) => {
let mut args = slice_vec_to_string_vec(args);
if !args.is_empty() {
let len = args.len() - 1;
args[len] = args[len].to_uppercase();
Some(Command {c_type, args: Some(args) })
} else {
Some(Command { c_type, args: None })
}
}
None => None,
}
}
fn slice_vec_to_string_vec(s: Vec<&str>) -> Vec<String> {
let mut tmp: Vec<String> = Vec::new();
for i in s[1..].iter() {
tmp.push(String::from(*i));
}
tmp
}
fn command_executor(cmd: Command, table: &mut HashMap<String, Vec<String>>) -> bool {
match cmd.c_type {
CommandType::Add => {
match cmd.args {
Some(arg) => {
add_employee(table, String::from(&arg[0][..]), String::from(&arg[2][..]))
}
None => println!("Invalid arguments..."),
}
true
}
CommandType::List => {
match cmd.args {
Some(arg) => list_all_employees(table, Some(String::from(&arg[0]))),
None => list_all_employees(table, None),
}
true
}
CommandType::Del => {
match cmd.args {
Some(arg) => {
del_employee(table, String::from(&arg[0][..]), String::from(&arg[2][..]))
}
None => println!("Invalid arguments..."),
}
true
}
CommandType::Quit => {
println!("Thank you for using this shitty system. Goodbye!");
false
}
}
}
fn list_all_employees(table: &HashMap<String, Vec<String>>, opts: Option<String>) {
match opts {
Some(opt) => match table.get(&opt) {
Some(values) => println!("\n{}: {:#?}", opt, values),
None => println!("{} is an invalid department.", opt),
},
None => println!("{:#?}", table),
}
}
fn add_employee(table: &mut HashMap<String, Vec<String>>, emp: String, dep: String) {
match table.get_mut(&dep) {
Some(ptr) => ptr.push(emp),
None => println!(
"Failed to get a pointer to the department: {} using employee: {}",
dep, emp
),
};
}
fn del_employee(table: &mut HashMap<String, Vec<String>>, emp: String, dep: String) {
match table.get_mut(&dep) {
Some(d_ptr) => match d_ptr.iter().position(|e| *e == emp) {
Some(e_ptr) => {
d_ptr.remove(e_ptr); // Has to have semi colon so that the arm doesn't return anything
}
None => println!("Unable to remove that element..."),
},
None => println!(
"Failed to get a pointer to department: {} using employee: {}",
dep, emp
),
};
}
```