From 4614c85b5e121b9dbc6463305a5c172596c43cd2 Mon Sep 17 00:00:00 2001 From: Emeka Allison Date: Sun, 8 Feb 2026 21:45:26 +0100 Subject: [PATCH 1/3] Add Week 2 Day 5 task for Emeka Allison expense-tracker --- .../day-5/emeka-expense-tracker/Cargo.lock | 7 + .../day-5/emeka-expense-tracker/Cargo.toml | 6 + .../emeka-expense-tracker/src/actions.rs | 122 ++++++++++++++++++ .../emeka-expense-tracker/src/expenses.rs | 86 ++++++++++++ .../day-5/emeka-expense-tracker/src/lib.rs | 2 + .../day-5/emeka-expense-tracker/src/main.rs | 43 ++++++ 6 files changed, 266 insertions(+) create mode 100644 submissions/week-2/day-5/emeka-expense-tracker/Cargo.lock create mode 100644 submissions/week-2/day-5/emeka-expense-tracker/Cargo.toml create mode 100644 submissions/week-2/day-5/emeka-expense-tracker/src/actions.rs create mode 100644 submissions/week-2/day-5/emeka-expense-tracker/src/expenses.rs create mode 100644 submissions/week-2/day-5/emeka-expense-tracker/src/lib.rs create mode 100644 submissions/week-2/day-5/emeka-expense-tracker/src/main.rs 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..d9a63da --- /dev/null +++ b/submissions/week-2/day-5/emeka-expense-tracker/src/actions.rs @@ -0,0 +1,122 @@ +use crate::expenses::ExpenseTracker; +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.replace(" ", "").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."); + } +} 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..2e740ab --- /dev/null +++ b/submissions/week-2/day-5/emeka-expense-tracker/src/lib.rs @@ -0,0 +1,2 @@ +pub mod actions; +pub mod expenses; 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..9eb3e9b --- /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, 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 => println!("Report feature is not implemented yet."), + ActionTypes::Quit => { + println!("Goodbye!"); + break; + } + } + println!("-----------------------------------"); + } +} From 2740ab447218247fecf75718596a75a537265fa3 Mon Sep 17 00:00:00 2001 From: Emeka Allison Date: Sun, 8 Feb 2026 22:21:50 +0100 Subject: [PATCH 2/3] Integrated writing to text file --- .../emeka-expense-tracker/src/actions.rs | 17 ++++++++++++- .../day-5/emeka-expense-tracker/src/lib.rs | 1 + .../day-5/emeka-expense-tracker/src/main.rs | 4 ++-- .../day-5/emeka-expense-tracker/src/record.rs | 24 +++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 submissions/week-2/day-5/emeka-expense-tracker/src/record.rs 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 index d9a63da..f953c88 100644 --- a/submissions/week-2/day-5/emeka-expense-tracker/src/actions.rs +++ b/submissions/week-2/day-5/emeka-expense-tracker/src/actions.rs @@ -1,4 +1,5 @@ use crate::expenses::ExpenseTracker; +use crate::record::write_to_report; use std::collections::HashMap; use std::io; @@ -56,7 +57,7 @@ fn get_list_of_items() -> HashMap { ); let user_input = read_user_input(); let mut items: HashMap = HashMap::new(); - for item in user_input.replace(" ", "").split(',') { + 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(); @@ -120,3 +121,17 @@ pub fn view_expense(tracker: &ExpenseTracker) { 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/lib.rs b/submissions/week-2/day-5/emeka-expense-tracker/src/lib.rs index 2e740ab..0df78ef 100644 --- a/submissions/week-2/day-5/emeka-expense-tracker/src/lib.rs +++ b/submissions/week-2/day-5/emeka-expense-tracker/src/lib.rs @@ -1,2 +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 index 9eb3e9b..9b4c799 100644 --- a/submissions/week-2/day-5/emeka-expense-tracker/src/main.rs +++ b/submissions/week-2/day-5/emeka-expense-tracker/src/main.rs @@ -1,5 +1,5 @@ use emeka_expense_tracker::actions::{ - ActionTypes, add_expense, get_all_expenses, remove_expense, show_total_expenses, + ActionTypes, add_expense, get_all_expenses, print_report, remove_expense, show_total_expenses, update_expense, view_expense, }; use emeka_expense_tracker::expenses::ExpenseTracker; @@ -32,7 +32,7 @@ fn main() { ActionTypes::ViewExpense => view_expense(&tracker), ActionTypes::GetAllExpenses => get_all_expenses(&tracker), ActionTypes::GetTotalExpenses => show_total_expenses(&tracker), - ActionTypes::PrintReport => println!("Report feature is not implemented yet."), + ActionTypes::PrintReport => print_report(&tracker), ActionTypes::Quit => { println!("Goodbye!"); break; 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..fceb285 --- /dev/null +++ b/submissions/week-2/day-5/emeka-expense-tracker/src/record.rs @@ -0,0 +1,24 @@ +use crate::expenses::Expense; +use std::fs::File; +use std::io::{Result, Write}; + +pub fn write_to_report(expenses: &Vec, total: f64) -> Result<()> { + let mut file = File::create("report.txt")?; + 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(()) +} From d4a61651897c476483174ee2e5b24e1f3daac1b0 Mon Sep 17 00:00:00 2001 From: Emeka Allison Date: Mon, 9 Feb 2026 12:17:37 +0100 Subject: [PATCH 3/3] Clean up and refactors here and there --- submissions/week-2/day-5/emeka-expense-tracker/.gitignore | 1 + submissions/week-2/day-5/emeka-expense-tracker/src/record.rs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 submissions/week-2/day-5/emeka-expense-tracker/.gitignore 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/src/record.rs b/submissions/week-2/day-5/emeka-expense-tracker/src/record.rs index fceb285..24f8dd4 100644 --- a/submissions/week-2/day-5/emeka-expense-tracker/src/record.rs +++ b/submissions/week-2/day-5/emeka-expense-tracker/src/record.rs @@ -2,8 +2,10 @@ 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.txt")?; + let mut file = File::create(REPORT_FILE)?; for (index, expense) in expenses.iter().enumerate() { let items_str = expense .items