Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions submissions/week-2/day-5/emeka-expense-tracker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
report.txt
7 changes: 7 additions & 0 deletions submissions/week-2/day-5/emeka-expense-tracker/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions submissions/week-2/day-5/emeka-expense-tracker/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "emeka-expense-tracker"
version = "0.1.0"
edition = "2024"

[dependencies]
137 changes: 137 additions & 0 deletions submissions/week-2/day-5/emeka-expense-tracker/src/actions.rs
Original file line number Diff line number Diff line change
@@ -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<String, f64> {
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<String, f64> = 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::<f64>() {
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}"),
}
}
}
86 changes: 86 additions & 0 deletions submissions/week-2/day-5/emeka-expense-tracker/src/expenses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use std::collections::HashMap;

#[derive(Debug)]
pub struct Expense {
id: u16,
pub items: HashMap<String, f64>,
pub total: f64,
}

#[derive(Debug)]
pub struct ExpenseTracker {
expenses: Vec<Expense>,
next_id: u16,
}

impl Expense {
pub fn create(assigned_id: u16, items: HashMap<String, f64>) -> 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<String, f64>) {
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<String, f64>) {
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<String, f64>) -> 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<Expense> {
&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()
}
}
3 changes: 3 additions & 0 deletions submissions/week-2/day-5/emeka-expense-tracker/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod actions;
pub mod expenses;
pub mod record;
43 changes: 43 additions & 0 deletions submissions/week-2/day-5/emeka-expense-tracker/src/main.rs
Original file line number Diff line number Diff line change
@@ -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!("-----------------------------------");
}
}
26 changes: 26 additions & 0 deletions submissions/week-2/day-5/emeka-expense-tracker/src/record.rs
Original file line number Diff line number Diff line change
@@ -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<Expense>, 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::<Vec<String>>()
.join(", ");
writeln!(
file,
"Expense {}: Total: ₦{:.2} --- Items: {}",
index + 1,
expense.total,
items_str
)?;
}
writeln!(file, "\nTotal Expenses: ₦{:.2}", total)?;
Ok(())
}