4
\$\begingroup\$

I've decided to start working on a budget application for the purposes of getting started on Rust. In its current form this program takes given expenses and income and tells the user whether they have a surplus, a deficit, or if they are breaking even. I will add more functionality to this program as I move along in the development process.

Did I respect every best practice in Rust ? Is there something I could improve ?

use std::io;
use std::process;

fn income_ask() -> String {
    println!("Add income? [y/n]: ");
    let mut add_income = String::new();
    io::stdin()
        .read_line(&mut add_income)
        .expect("Failed to read input");
    return add_income;
}

fn income_sum(income_list: &Vec<u64>, income: &mut u64) {
    *income = income_list.iter().sum();
    println!("{}", income);
}

fn expense_ask() -> String {
    println!("Add expense? [y/n]: ");
    let mut add_expense = String::new();
    io::stdin()
        .read_line(&mut add_expense)
        .expect("Failed to read input");
    return add_expense;
}

fn expense_sum(expense_list: &Vec<u64>, expenses: &mut u64) {
    *expenses = expense_list.iter().sum();
}

fn prompt_income(income_list: &mut Vec<u64>, income_name: &mut Vec<String>, income: &mut u64) {
    loop {
        let result = income_ask();
        if result.trim() == "y" {
            println!("Enter source of income. [Numbers Only]: ");
            let mut income_input = String::new();
            io::stdin()
                .read_line(&mut income_input)
                .expect("Failed to read input");
            let income_input: u64 = match income_input.trim().parse() {
                Ok(num) => num,
                Err(_) => continue,
            };
            income_list.push(income_input);

            println!("Enter income name. [Name Only]: ");
            let mut income_name1 = String::new();
            io::stdin()
                .read_line(&mut income_name1)
                .expect("Failed to read input");
            income_name.push(income_name1);
        } else {
            break;
        }
    }
    income_sum(&income_list, income);
    println!("Total user income: {} ", income);
}

fn prompt_expense(expense_list: &mut Vec<u64>, expense_name: &mut Vec<String>, expenses: &mut u64) {
    loop {
        let result = expense_ask();
        if result.trim() == "y" {
            println!("Enter expense amount. [Numbers Only]: ");
            let mut expense_input = String::new();
            io::stdin()
                .read_line(&mut expense_input)
                .expect("Failed to read input");
            let expense_input: u64 = match expense_input.trim().parse() {
                Ok(num) => num,
                Err(_) => continue,
            };
            expense_list.push(expense_input);

            println!("Enter expense name. [Name Only]: ");
            let mut expense_name1 = String::new();
            io::stdin()
                .read_line(&mut expense_name1)
                .expect("Failed to read input");
            expense_name.push(expense_name1);
        } else {
            break;
        }
        expense_sum(&expense_list, expenses);
        println!("Total user expenses: {}", expenses);
    }
}

fn uservalue(income: u64, expenses: u64) {
    let valoutput: i64;
    valoutput = income as i64 - expenses as i64;
    if valoutput < 0 {
        println!(
            "You are in the negative, you have a deficit of {}",
            valoutput
        );
    } else if valoutput == 0 {
        println!("You have broken even, you are spending exactly as much as you make.");
    } else if valoutput > 0 {
        println!(
            "You are in the positive, you have a surplus of {}",
            valoutput
        );
    }
}

fn close_program() {
    println!("Exiting Program.");
    process::exit(1);
}

fn main() {
    let mut expense_list: Vec<u64> = Vec::new();
    let mut income_list: Vec<u64> = Vec::new();
    let mut expense_name: Vec<String> = Vec::new();
    let mut income_name: Vec<String> = Vec::new();
    let mut income: u64 = 0;
    let mut expenses: u64 = 0;

    loop {
        prompt_income(&mut income_list, &mut income_name, &mut income);
        prompt_expense(&mut expense_list, &mut expense_name, &mut expenses);

        uservalue(income, expenses);
        println!("Would you like to run another analysis? [y/n]: ");
        let mut another = String::new();
        io::stdin()
            .read_line(&mut another)
            .expect("Failed to read input");

        if another.trim() == "y" {
            income = 0;
            expenses = 0;
            expense_list.clear();
            income_list.clear();
            expense_name.clear();
            income_name.clear();
        } else {
            close_program();
        }
    }
}
\$\endgroup\$
1
\$\begingroup\$

I rewrote some of your program to make it more idiomatic in Rust. I tried to comment significant changes I made, but if you have any questions about why I changed things, you can click "add a comment" below my answer. Side note: the comments I made aren't necessarily good code comments (as in I wouldn't keep them in the code, they're just to explain to you the changes)

// Pulling out this repeated code into one function reduces repetition.
fn ask(buf: &mut String, prompt: &str, guide: &str) {
    use std::io::{stdin, stdout, Write};

    // Using print (not println) lets you have the input seen on the same line.
    print!("{} [{}]: ", prompt, guide);
    // Since stdout is line-buffered, you must flush if you are not printing a new line.
    stdout().flush().expect("Failed to flush output");
    stdin().read_line(buf).expect("Failed to read input");
}

fn ask_name(prompt: &str) -> String {
    let mut buf = String::new();
    ask(&mut buf, prompt, "Name only");
    buf
}

// Here we keep asking until we get a valid number.
fn ask_numeric(prompt: &str) -> u64 {
    let mut buf = String::new();
    loop {
        ask(&mut buf, prompt, "Numbers only");
        if let Ok(num) = buf.trim().parse() {
            return num;
        }
        buf.clear();
        println!("Try again.");
    }
}

// Here we may also want to keep asking until we get either y/n, to be more exact.
fn ask_yn(prompt: &str) -> bool {
    let mut buf = String::new();
    ask(&mut buf, prompt, "y/n");
    buf.trim().eq_ignore_ascii_case("y")
}

// By grouping the name and amount for each entry, we make it more explicit that the vec of names
// and vec of amounts are paired together. We could have just used a tuple like (String, u64), but
// since this is what the program revolves around, its certainly worth making a struct for.
#[derive(Clone, Debug)]
struct Record {
    name: String,
    amount: u64,
}

fn prompt_income(income_list: &mut Vec<Record>) -> u64 {
    // Using our ask functions simplify this loop greatly.
    while ask_yn("Add income?") {
        income_list.push(Record {
            amount: ask_numeric("Enter income amount."),
            name: ask_name("Enter income name."),
        });
    }
    // Here we map from Record to u64, so that we sum the amounts.
    let income = income_list.iter().map(|r| r.amount).sum();
    println!("Total user income: {} ", income);
    income
}

// Note that this function is almost identical to prompt_income, this would be another good target
// to deduplicate.
fn prompt_expense(expense_list: &mut Vec<Record>) -> u64 {
    while ask_yn("Add expense?") {
        expense_list.push(Record {
            amount: ask_numeric("Enter expense amount."),
            name: ask_name("Enter expense name."),
        });
    }
    let expenses = expense_list.iter().map(|r| r.amount).sum();
    println!("Total user expenses: {} ", expenses);
    expenses
}

fn uservalue(income: u64, expenses: u64) {
    use std::cmp::Ordering;

    // Here we always subtract the smaller number from the bigger one, which keeps us from having
    // to worry about casting to signed types and having any problems with overflow, etc. We
    // compare the numbers first, then do different things based on that.
    match income.cmp(&expenses) {
        Ordering::Less => {
            println!(
                "You are in the negative, you have a deficit of {}",
                expenses - income
            );
        }
        Ordering::Equal => {
            println!("You have broken even, you are spending exactly as much as you make.")
        }
        Ordering::Greater => {
            println!(
                "You are in the positive, you have a surplus of {}",
                income - expenses
            );
        }
    }
}

fn main() {
    let mut expense_list = Vec::new();
    let mut income_list = Vec::new();

    loop {
        let income = prompt_income(&mut income_list);
        let expenses = prompt_expense(&mut expense_list);

        uservalue(income, expenses);

        if !ask_yn("Would you like to run another analysis?") {
            // This break gets us out of this loop and continues main, eventually just exiting
            // the program normally.
            break;
        }

        expense_list.clear();
        income_list.clear();
    }

    // In the close_program function you had, you were exiting with status 1 which actually
    // signifies an error. By just letting main end, we exit normally. You could put a println here
    // if you wanted.
}
\$\endgroup\$
  • \$\begingroup\$ Thank you very much for your input! I will take a look into it! \$\endgroup\$ – strangethingspy Jul 19 at 21:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.