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.
I implemented a solution for the third exercise of Chapter 8 of The Book. I'm still trying to build my intuition for what makes a good Rust program and so would really appreciate some pointers to help me improve. Here's my current solution:
// 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.
// @todo Add functions to remove people, departments and companies. Add a clear function.
use std::collections::HashMap;
use std::collections::HashSet;
use std::io;
use std::io::Write;
#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
struct Employee(String);
#[derive(Debug)]
struct Department(HashSet<Employee>);
#[derive(Debug)]
struct Company(HashMap<String, Department>);
fn make_company() -> Company {
Company(HashMap::new())
}
#[derive(Debug)]
struct ExistingDepartment(String);
// This assumes all department names are unique.
fn add_department(company: &mut Company, department: &str) -> Result<(), ExistingDepartment> {
let department_name = String::from(department);
match company.0.contains_key(&department_name) {
true => Err(ExistingDepartment(department_name)),
false => {
company
.0
.insert(department_name, Department(HashSet::<Employee>::new()));
Ok(())
}
}
}
#[derive(Debug)]
enum AddEmployeeError {
NonexistentDepartment(String),
ExistingEmployee(Employee),
}
// This assumes that all employee names in a department are unique.
fn add_employee(
company: &mut Company,
department_name: &str,
employee: Employee,
) -> Result<(), AddEmployeeError> {
match company.0.get_mut(department_name) {
None => Err(AddEmployeeError::NonexistentDepartment(String::from(
department_name,
))),
Some(department) => match department.0.insert(employee.clone()) {
true => Ok(()),
false => Err(AddEmployeeError::ExistingEmployee(employee)),
},
}
}
#[derive(Debug)]
struct NonexistentDepartment(String);
fn get_employees_department(
company: &Company,
department_name: &str,
) -> Result<Vec<Employee>, NonexistentDepartment> {
match company.0.get(department_name) {
None => Err(NonexistentDepartment(String::from(department_name))),
Some(department) => {
let mut result: Vec<Employee> = department.0.iter().cloned().collect();
result.sort_unstable();
Ok(result)
}
}
}
fn get_employees_company(company: &Company) -> Vec<Employee> {
let mut result = company
.0
.keys()
.map(|department| get_employees_department(company, department).unwrap())
.collect::<Vec<Vec<Employee>>>()
.concat();
result.sort_unstable();
result
}
fn format_slice(slice: &[Employee]) -> Option<String> {
match slice.len() {
0 => None,
_ => Some(
slice
.iter()
.map(|employee| employee.0.to_string())
.collect::<Vec<String>>()
.join("\n"),
),
}
}
#[derive(PartialEq)]
enum GetInput {
Print,
Read,
Parse,
}
#[derive(PartialEq)]
enum Mode {
Greet,
GetInput(GetInput),
Quit,
}
struct State {
mode: Mode,
company: Company,
input: String,
}
fn make_state() -> State {
State {
company: make_company(),
mode: Mode::Greet,
input: String::new(),
}
}
fn print_flush(input: &str) -> Result<(), std::io::Error> {
print!("{}", input);
io::stdout().flush()
}
fn read_line(string: &mut String) -> Result<usize, std::io::Error> {
string.clear();
io::stdin().read_line(string)
}
// This assumes that department and employee names don't have whitespace.
fn process_input(state: &mut State) {
let company = &mut state.company;
match state.input.len() {
0 => {
state.mode = Mode::GetInput(GetInput::Print);
}
_ => {
let tokens: Vec<&str> = state.input.split_whitespace().collect();
match tokens.get(0) {
None => {
state.mode = Mode::GetInput(GetInput::Print);
}
Some(token) => match *token {
"quit" => {
println!("Bye for now!");
state.mode = Mode::Quit;
}
command => {
match command {
"add-department" => match tokens[1..].len() {
0 => println!("Command \"{}\" requires at least 1 argument.", command),
_ => tokens[1..].iter().for_each(|element| match add_department(company, element) {
Ok(_) => (),
Err(error) => println!("Error: {:?}", error)
})
},
"add-employee" => match tokens[1..].len() {
0 ..= 1 => println!("Command \"{}\" requires at least 2 arguments.", command),
_ => match company.0.contains_key(tokens[1]) {
true => tokens[2..].iter().for_each(|employee| match add_employee(company, tokens[1], Employee(employee.to_string())) {
Ok(_) => (),
Err(error) => println!("Error: {:?}", error)
}),
false => println!("Department \"{}\" doesn't exist.", tokens[1])
}
},
"get-employees" => match tokens[1..].len() {
0 => println!("Command \"{}\" requires at least 1 argument.", command),
_ => tokens[1..].iter().for_each(|department| match company.0.contains_key(*department) {
true => match format_slice(&get_employees_department(company, department).unwrap()) {
None => println!("Department \"{}\" has no employees.", department),
Some(employees) => println!(
"Employees in \"{}\":
{}", department, employees)
},
false => println!("Department \"{}\" doesn't exist.", department)
})
},
"get-employees-all" => match format_slice(&get_employees_company(company)) {
None => println!("Company has no employees."),
Some(employees) => println!(
"Employees:
{}", employees)
},
"help" => println!(
"Commands:
add-department <department_name [department_name ...]>
Add one or more departments to the company.
add-employee <department_name> <employee_name [employee_name ...]>
Add one or more employees to a department.
get-employees <department_name [department_name ...]>
Print a list of employees from one or more departments.
get-employees-all
Print a list of all employees in the company.
help
Print this message.
quit
Quit."
),
command => {
println!("Invalid command \"{}\".", command);
}
}
state.mode = Mode::GetInput(GetInput::Print);
}
},
}
}
}
}
fn state_machine(state: &mut State) {
match &state.mode {
Mode::Greet => {
println!(
"Database program.
Enter \"help\" for a list of commands."
);
state.mode = Mode::GetInput(GetInput::Print);
}
Mode::GetInput(get_input_step) => match get_input_step {
GetInput::Print => {
println!("Company: {:?}", state.company);
match print_flush("> ") {
Ok(_) => {
state.mode = Mode::GetInput(GetInput::Read);
}
Err(error) => println!("Error {:?}", error),
}
}
GetInput::Read => match read_line(&mut state.input) {
Ok(_) => {
state.mode = Mode::GetInput(GetInput::Parse);
}
Err(error) => println!("Error: {:?}", error),
},
GetInput::Parse => process_input(state),
},
Mode::Quit => (),
}
}
fn repl(function: fn(&mut State), state: &mut State) {
while state.mode != Mode::Quit {
function(state)
}
}
fn main() {
repl(state_machine, &mut make_state())
}
I previously tried to refactor the process_input
function to try to decrease nesting by moving parts of it to different functions, but I ran across borrowing issues so I stopped trying. I also couldn't figure out a nice way to implement it such that it accepts inputs that use the same format as the example (i.e., "Add Amir to Sales"), so I did it a little differently.
I'm also not sure if I'm using the Result
type nicely and if the tuple structs and enum I made to pass to them are a good way to do error handling.
I also feel like some of the cloning I did is probably unnecessary, but I'm not sure how I could refactor them away, so it would be nice to know if that is indeed the case and how I might refactor. In fact, I think it would be helpful to know how I might determine when it is appropriate to clone or when to borrow, as I think I'm a little iffy about that topic.
On a related note, how should I go about writing functions? Is it generally better to initially write functions such that they clone their arguments, then try to refactor the cloning away later on, or to try to borrow everything up front then try to fix any borrow-checker issues that crop up and only clone if I can't fix them?
In any case, I'd really appreciate any constructive criticisms and advice.