diff --git a/submissions/week-2/day-5/emeka-expense-tracker/.gitignore b/submissions/week-2/day-5/emeka-expense-tracker/.gitignore new file mode 100644 index 0000000..32d27f6 --- /dev/null +++ b/submissions/week-2/day-5/emeka-expense-tracker/.gitignore @@ -0,0 +1 @@ +report.txt diff --git a/submissions/week-2/day-5/emeka-expense-tracker/Cargo.lock b/submissions/week-2/day-5/emeka-expense-tracker/Cargo.lock new file mode 100644 index 0000000..44e4085 --- /dev/null +++ b/submissions/week-2/day-5/emeka-expense-tracker/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "emeka-expense-tracker" +version = "0.1.0" diff --git a/submissions/week-2/day-5/emeka-expense-tracker/Cargo.toml b/submissions/week-2/day-5/emeka-expense-tracker/Cargo.toml new file mode 100644 index 0000000..1aab082 --- /dev/null +++ b/submissions/week-2/day-5/emeka-expense-tracker/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "emeka-expense-tracker" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/submissions/week-2/day-5/emeka-expense-tracker/src/actions.rs b/submissions/week-2/day-5/emeka-expense-tracker/src/actions.rs new file mode 100644 index 0000000..f953c88 --- /dev/null +++ b/submissions/week-2/day-5/emeka-expense-tracker/src/actions.rs @@ -0,0 +1,137 @@ +use crate::expenses::ExpenseTracker; +use crate::record::write_to_report; +use std::collections::HashMap; +use std::io; + +#[derive(Clone, Debug)] +pub enum ActionTypes { + AddExpense, + RemoveExpense, + UpdateExpense, + ViewExpense, + GetAllExpenses, + GetTotalExpenses, + PrintReport, + Quit, +} + +fn read_user_input() -> String { + let mut user_input = String::new(); + io::stdin() + .read_line(&mut user_input) + .expect("Failed to read input"); + user_input.trim().to_string() +} + +fn get_numeric_id() -> u16 { + let id_input = read_user_input(); + let id: u16 = match id_input.parse() { + Ok(num) => num, + Err(_) => { + println!("Invalid ID. Please enter a valid number."); + get_numeric_id() + } + }; + id +} + +impl ActionTypes { + pub fn get_action(command_map: &HashMap<&str, ActionTypes>) -> ActionTypes { + println!("\nWhat would you like to do?"); + println!( + "Enter any of the following commands:\n\"add\", \"remove\", \"view\", \"all\", \"total\", \"report\", \"update\", or \"q\" to quit" + ); + let user_input = read_user_input().to_lowercase(); + if let Some(action) = command_map.get(user_input.as_str()) { + action.clone() + } else { + println!("Invalid command. Please try again."); + ActionTypes::get_action(command_map) + } + } +} + +fn get_list_of_items() -> HashMap { + println!( + "Enter items in the format: item_name:cost, separated by commas. e.g. \"coffee:3.5,lunch:12.0,groceries:45.0\"" + ); + let user_input = read_user_input(); + let mut items: HashMap = HashMap::new(); + for item in user_input.split(',') { + let parts: Vec<&str> = item.trim().split(':').collect(); + if parts.len() == 2 { + let name = parts[0].trim().to_string(); + if let Ok(cost) = parts[1].trim().parse::() { + items.insert(name, cost); + } else { + println!("Invalid cost for item '{name}'. Skipping."); + } + } else { + println!("Invalid format for item '{item}'. Skipping."); + } + } + items +} + +pub fn show_total_expenses(tracker: &ExpenseTracker) { + let total = tracker.get_total_expenses(); + println!("Your total expenses amount to: ₦{total:.2}"); +} + +pub fn add_expense(tracker: &mut ExpenseTracker) { + println!("Add a list of items and their cost to your expense tracker"); + let items = get_list_of_items(); + println!("You added the following items:"); + tracker.add_expense(items); +} + +pub fn get_all_expenses(tracker: &ExpenseTracker) { + let expenses = tracker.get_expenses(); + println!("Here are your total expenses:\n {expenses:#?}"); +} + +pub fn remove_expense(tracker: &mut ExpenseTracker) { + println!("Enter the ID of the expense you want to remove:"); + let id = get_numeric_id(); + if tracker.remove_expense(id) { + println!("Expense with ID {id} removed successfully."); + } else { + println!("Expense with ID {id} not found."); + } +} + +pub fn update_expense(tracker: &mut ExpenseTracker) { + println!("Enter the ID of the expense you want to update:"); + let id = get_numeric_id(); + println!("Update an expense by providing a list of items and their cost"); + let items = get_list_of_items(); + if tracker.edit_expense(id, items) { + println!("Expense with ID {id} updated successfully."); + } else { + println!("Expense with ID {id} not found."); + } +} + +pub fn view_expense(tracker: &ExpenseTracker) { + println!("Enter the ID of the expense you want to view:"); + let id = get_numeric_id(); + if let Some(expense) = tracker.get_expense_by_id(id) { + println!("Expense with ID {id}:\n{expense:#?}"); + } else { + println!("Expense with ID {id} not found."); + } +} + +pub fn print_report(tracker: &ExpenseTracker) { + let expenses = tracker.get_expenses(); + if expenses.is_empty() { + println!("No expenses to report."); + } else { + let total_expense = tracker.get_total_expenses(); + println!("Generating report..."); + match write_to_report(expenses, total_expense) { + Ok(_) => println!("Report generated successfully as 'report.txt'."), + Err(e) => println!("Failed to generate report: {e}"), + } + } +} diff --git a/submissions/week-2/day-5/emeka-expense-tracker/src/expenses.rs b/submissions/week-2/day-5/emeka-expense-tracker/src/expenses.rs new file mode 100644 index 0000000..d995033 --- /dev/null +++ b/submissions/week-2/day-5/emeka-expense-tracker/src/expenses.rs @@ -0,0 +1,86 @@ +use std::collections::HashMap; + +#[derive(Debug)] +pub struct Expense { + id: u16, + pub items: HashMap, + pub total: f64, +} + +#[derive(Debug)] +pub struct ExpenseTracker { + expenses: Vec, + next_id: u16, +} + +impl Expense { + pub fn create(assigned_id: u16, items: HashMap) -> Self { + let total = items.values().sum(); + Expense { + id: assigned_id, + items, + total, + } + } + + fn sum_total(&mut self) { + self.total = self.items.values().sum(); + } + + pub fn edit_items(&mut self, items: HashMap) { + for (key, new_value) in items { + self.items.insert(key, new_value); + } + self.sum_total(); + } + + pub fn remove_item(&mut self, item_name: &str) { + self.items.remove(item_name); + self.sum_total(); + } +} + +impl ExpenseTracker { + pub fn start() -> Self { + ExpenseTracker { + expenses: Vec::new(), + next_id: 1, + } + } + + pub fn add_expense(&mut self, items: HashMap) { + let expense = Expense::create(self.next_id, items); + self.expenses.push(expense); + self.next_id += 1; + } + + pub fn edit_expense(&mut self, id: u16, items: HashMap) -> bool { + if let Some(expense) = self.expenses.iter_mut().find(|e| e.id == id) { + expense.edit_items(items); + true + } else { + false + } + } + + pub fn remove_expense(&mut self, id: u16) -> bool { + if let Some(pos) = self.expenses.iter().position(|e| e.id == id) { + self.expenses.remove(pos); + true + } else { + false + } + } + + pub fn get_expenses(&self) -> &Vec { + &self.expenses + } + + pub fn get_expense_by_id(&self, id: u16) -> Option<&Expense> { + self.expenses.iter().find(|e| e.id == id) + } + + pub fn get_total_expenses(&self) -> f64 { + self.expenses.iter().map(|e| e.total).sum() + } +} diff --git a/submissions/week-2/day-5/emeka-expense-tracker/src/lib.rs b/submissions/week-2/day-5/emeka-expense-tracker/src/lib.rs new file mode 100644 index 0000000..0df78ef --- /dev/null +++ b/submissions/week-2/day-5/emeka-expense-tracker/src/lib.rs @@ -0,0 +1,3 @@ +pub mod actions; +pub mod expenses; +pub mod record; diff --git a/submissions/week-2/day-5/emeka-expense-tracker/src/main.rs b/submissions/week-2/day-5/emeka-expense-tracker/src/main.rs new file mode 100644 index 0000000..9b4c799 --- /dev/null +++ b/submissions/week-2/day-5/emeka-expense-tracker/src/main.rs @@ -0,0 +1,43 @@ +use emeka_expense_tracker::actions::{ + ActionTypes, add_expense, get_all_expenses, print_report, remove_expense, show_total_expenses, + update_expense, view_expense, +}; +use emeka_expense_tracker::expenses::ExpenseTracker; +use std::collections::HashMap; + +fn main() { + let command_map: HashMap<&str, ActionTypes> = HashMap::from([ + ("add", ActionTypes::AddExpense), + ("remove", ActionTypes::RemoveExpense), + ("update", ActionTypes::UpdateExpense), + ("view", ActionTypes::ViewExpense), + ("all", ActionTypes::GetAllExpenses), + ("total", ActionTypes::GetTotalExpenses), + ("report", ActionTypes::PrintReport), + ("q", ActionTypes::Quit), + ]); + let mut tracker = ExpenseTracker::start(); + println!("Welcome to your expense tracker!"); + println!("You can add expenses, edit them, and remove items from them."); + + loop { + let action = ActionTypes::get_action(&command_map); + println!("\nYou chose {action:?}"); + println!("-----------------------------------\n"); + + match action { + ActionTypes::AddExpense => add_expense(&mut tracker), + ActionTypes::RemoveExpense => remove_expense(&mut tracker), + ActionTypes::UpdateExpense => update_expense(&mut tracker), + ActionTypes::ViewExpense => view_expense(&tracker), + ActionTypes::GetAllExpenses => get_all_expenses(&tracker), + ActionTypes::GetTotalExpenses => show_total_expenses(&tracker), + ActionTypes::PrintReport => print_report(&tracker), + ActionTypes::Quit => { + println!("Goodbye!"); + break; + } + } + println!("-----------------------------------"); + } +} diff --git a/submissions/week-2/day-5/emeka-expense-tracker/src/record.rs b/submissions/week-2/day-5/emeka-expense-tracker/src/record.rs new file mode 100644 index 0000000..24f8dd4 --- /dev/null +++ b/submissions/week-2/day-5/emeka-expense-tracker/src/record.rs @@ -0,0 +1,26 @@ +use crate::expenses::Expense; +use std::fs::File; +use std::io::{Result, Write}; + +const REPORT_FILE: &str = "report.txt"; + +pub fn write_to_report(expenses: &Vec, total: f64) -> Result<()> { + let mut file = File::create(REPORT_FILE)?; + for (index, expense) in expenses.iter().enumerate() { + let items_str = expense + .items + .iter() + .map(|(item, price)| format!("{item}: ₦{price:.2}")) + .collect::>() + .join(", "); + writeln!( + file, + "Expense {}: Total: ₦{:.2} --- Items: {}", + index + 1, + expense.total, + items_str + )?; + } + writeln!(file, "\nTotal Expenses: ₦{:.2}", total)?; + Ok(()) +}