diff --git a/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/.gitignore b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/.gitignore new file mode 100644 index 0000000..c41cc9e --- /dev/null +++ b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/.gitignore @@ -0,0 +1 @@ +/target \ No newline at end of file diff --git a/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/Cargo.lock b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/Cargo.lock new file mode 100644 index 0000000..76d54ae --- /dev/null +++ b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/Cargo.lock @@ -0,0 +1,107 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "expense-tracker" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "zmij" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/Cargo.toml b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/Cargo.toml new file mode 100644 index 0000000..53fd0b7 --- /dev/null +++ b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "expense-tracker" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = {version = "1.0.228", features = ["derive"]} +serde_json = "1.0.149" \ No newline at end of file diff --git a/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/README.md b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/README.md new file mode 100644 index 0000000..9fa1a97 --- /dev/null +++ b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/README.md @@ -0,0 +1,37 @@ +# Expense Tracker + +A simple CLI expense tracker built in Rust. It supports adding, viewing, updating, and removing expenses, and persists data to a JSON file between runs. + +## Assignment Requirements Checklist + +- Prints out operations a user can carry out. +- Program keeps running after reading, adding, updating, or deleting. +- User can quit the program with 'q' and is prompted y/n. +- After updating or adding, the user can see the values they just added. +- All interactions are written to a file (saved to JSON). +- Abstracted into modules. + +## How It Works + +- Startup loads existing data from `expenses.json`. +- Each expense has an `id`, `name`, `amount`, and `tx_type` (Credit/Debit). +- Data is saved back to `expenses.json` after add/update/delete and on quit. + +## Run + +```bash +cargo run +``` + +## Project Structure + +- `src/main.rs`: application entry point and menu loop. +- `src/menu.rs`: menu options and selection logic. +- `src/modules.rs`: feature modules (add/view/update/delete/quit). +- `src/tracker.rs`: core tracker data model and persistence. +- `expenses.json`: persisted data file. + +## Usage Tips + +- Enter the menu number (1-4) or name (`add`, `view`, `update`, `remove`), or `q` to quit. +- On quit, confirm with `y` or `n`. diff --git a/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/expenses.json b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/expenses.json new file mode 100644 index 0000000..b67053a --- /dev/null +++ b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/expenses.json @@ -0,0 +1,32 @@ +{ + "6": { + "id": 6, + "name": "Toy", + "amount": 22.0, + "tx_type": "Debit" + }, + "3": { + "id": 3, + "name": "Food", + "amount": 345.0, + "tx_type": "Credit" + }, + "4": { + "id": 4, + "name": "Meat", + "amount": 432.0, + "tx_type": "Debit" + }, + "5": { + "id": 5, + "name": "Fish", + "amount": 677.0, + "tx_type": "Debit" + }, + "1": { + "id": 1, + "name": "Cloth", + "amount": 555.0, + "tx_type": "Credit" + } +} \ No newline at end of file diff --git a/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/src/main.rs b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/src/main.rs new file mode 100644 index 0000000..0dbcce0 --- /dev/null +++ b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/src/main.rs @@ -0,0 +1,113 @@ +mod menu; +mod modules; +mod tracker; + +use menu::Menu; +use std::io; +use tracker::ExpenseTracker; + +fn main() { + let filename = "expenses.json"; + let mut tracker = ExpenseTracker::new(); + + modules::username_module(); + tracker.values = ExpenseTracker::load_from_file(filename); + if let Some(max_id) = tracker.values.keys().max() { + tracker.next_id = max_id + 1; + } + + loop { + menu_module(&mut tracker, filename); + } +} + +fn menu_module(tracker: &mut ExpenseTracker, filename: &str) { + let mut menu_input: String = String::new(); + + println!("Please select a menu number or name:"); + println!( + "\n 1. {:#?}\n 2. {:#?}\n 3. {:#?}\n 4. {:#?}\n q. {:#?} ", + Menu::Add, + Menu::View, + Menu::Update, + Menu::Remove, + Menu::Quit + ); + + io::stdin() + .read_line(&mut menu_input) + .expect("Failed To Get Input"); + + let result = match Menu::menu_select(&menu_input) { + Some(result) => result, + None => { + println!("Invalid Menu"); + return menu_module(tracker, filename); + } + }; + + match result { + Menu::Add => { + println!("-------------------------------------------------\n"); + println!("Add Module"); + println!("\n-------------------------------------------------\n"); + + let new_expenses = modules::add_module(tracker, filename); + tracker.save_to_file(filename); + + println!("-------------------------------------------------"); + println!("-------------------------------------------------"); + println!("New {:#?}", new_expenses); + println!("-------------------------------------------------"); + println!("-------------------------------------------------\n"); + } + Menu::View => { + println!("-------------------------------------------------\n"); + println!("View Module"); + println!("\n-------------------------------------------------\n"); + let all_expenses = modules::view_module(tracker); + + println!("-------------------------------------------------"); + println!("-------------------------------------------------"); + println!("All Expenses: {:#?}", all_expenses); + println!("-------------------------------------------------"); + println!("-------------------------------------------------\n"); + } + Menu::Update => { + println!("-------------------------------------------------\n"); + println!("Update Module"); + println!("\n-------------------------------------------------\n"); + + let updated_data = modules::update_module(tracker, filename); + tracker.save_to_file(filename); + + println!("-------------------------------------------------"); + println!("-------------------------------------------------"); + println!("Updated Data {:#?}", updated_data); + println!("-------------------------------------------------"); + println!("-------------------------------------------------\n"); + } + Menu::Remove => { + println!("-------------------------------------------------\n"); + println!("\n-------------------------------------------------\n"); + println!("Remove Module"); + + let result = modules::delete_module(tracker, filename); + tracker.save_to_file(filename); + println!("Expense Deleted: {result}"); + + println!("-------------------------------------------------"); + println!("-------------------------------------------------\n"); + } + Menu::Quit => { + println!("-------------------------------------------------\n"); + println!("\n-------------------------------------------------\n"); + + println!("Quit Module"); + + modules::quit_module(tracker, filename); + println!("\n-------------------------------------------------\n"); + println!("-------------------------------------------------\n"); + } + } +} diff --git a/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/src/menu.rs b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/src/menu.rs new file mode 100644 index 0000000..b3f3aab --- /dev/null +++ b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/src/menu.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum Menu { + Add, View, Update, Remove, Quit, +} + +impl Menu { + pub fn menu_select(selection: &str) -> Option { + let selection = selection.trim().to_lowercase(); + match selection.as_str() { + "add" | "1" => Some(Menu::Add), + "view" | "2" => Some(Menu::View), + "update" | "3" => Some(Menu::Update), + "remove" | "4" => Some(Menu::Remove), + "quit" | "q" => Some(Menu::Quit), + _ => None, + } + } +} \ No newline at end of file diff --git a/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/src/modules.rs b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/src/modules.rs new file mode 100644 index 0000000..44ca451 --- /dev/null +++ b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/src/modules.rs @@ -0,0 +1,127 @@ +use crate::tracker::{Expense, ExpenseTracker, TransactionType}; +use std::io; +use std::process::exit; + +pub fn username_module() -> String { + let mut username: String = String::new(); + + println!("Please enter Username"); + io::stdin() + .read_line(&mut username) + .expect("Failed To Get Input"); + + println!("Welcome {username}"); + username +} + +pub fn add_module(tracker: &mut ExpenseTracker, filename: &str) -> Expense { + let mut name = String::new(); + let mut amount = String::new(); + let mut tx_type = String::new(); + + println!("Enter Expense Name:"); + + io::stdin() + .read_line(&mut name) + .expect("Failed to get Input"); + println!("Enter Expense Amount: "); + io::stdin() + .read_line(&mut amount) + .expect("Failed to get Input"); + println!("Enter Transaction Type (Credit/Debit): "); + io::stdin() + .read_line(&mut tx_type) + .expect("Failed to get Input"); + + let name = name.trim().to_string(); + + let amount = amount + .trim() + .parse::() + .expect("Failed to parse amount"); + let tx_type = match tx_type.trim().to_lowercase().as_str() { + "credit" => TransactionType::Credit, + "debit" => TransactionType::Debit, + _ => { + println!("Invalid transaction type. Defaulting to Debit."); + TransactionType::Debit + } + }; + let add_result = tracker.add(name, amount, tx_type); + + tracker.save_to_file(filename); + + add_result +} + +pub fn view_module(tracker: &mut ExpenseTracker) -> Vec<&Expense> { + tracker.view_all() +} + +pub fn update_module(tracker: &mut ExpenseTracker, filename: &str) -> Expense { + let mut id = String::new(); + let mut amount = String::new(); + let mut tx_type = String::new(); + + println!("Enter Expense ID: "); + io::stdin().read_line(&mut id).expect("Failed to get Input"); + + println!("Enter Expense Amount: "); + io::stdin() + .read_line(&mut amount) + .expect("Failed to get Input"); + println!("Enter Transaction Type (Credit/Debit): "); + io::stdin() + .read_line(&mut tx_type) + .expect("Failed to get Input"); + + let id = id.trim().parse().expect("Failed to parse id"); + let amount = amount + .trim() + .parse::() + .expect("Failed to parse amount"); + let tx_type = match tx_type.trim().to_lowercase().as_str() { + "credit" => TransactionType::Credit, + "debit" => TransactionType::Debit, + _ => { + println!("Invalid transaction type. Defaulting to Debit."); + TransactionType::Debit + } + }; + + let update_result = tracker.update(id, amount, tx_type); + + tracker.save_to_file(filename); + + update_result +} + +pub fn delete_module(tracker: &mut ExpenseTracker, filename: &str) -> bool { + let mut id = String::new(); + + println!("Enter Expense ID: "); + io::stdin().read_line(&mut id).expect("Failed to get Input"); + let id = id.trim().parse().expect("Failed to parse id"); + + let delete_result =tracker.delete(id); + tracker.save_to_file(filename); + delete_result +} + +pub fn quit_module(tracker: &mut ExpenseTracker, filename: &str) { + let mut input = String::new(); + println!("Are you Sure You Want to Quit (Y/N)"); + io::stdin() + .read_line(&mut input) + .expect("Failed to get input"); + let input = input.trim().to_lowercase(); + + if input == "y" { + tracker.save_to_file(filename); + exit(1) + } else if input == "n" { + println!("Program Resumed ---------"); + } else { + println!("Invalid Input"); + } +} diff --git a/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/src/tracker.rs b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/src/tracker.rs new file mode 100644 index 0000000..3c70be0 --- /dev/null +++ b/submissions/week-2/day-5/Abel-Osaretin/expense-tracker/src/tracker.rs @@ -0,0 +1,84 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs::File; +use std::io::{Read, Write}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum TransactionType { + Credit, + Debit, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Expense { + pub id: u8, + pub name: String, + pub amount: f64, + pub tx_type: TransactionType, +} + +pub struct ExpenseTracker { + pub values: HashMap, + pub next_id: u8, +} + +impl ExpenseTracker { + pub fn new() -> Self { + Self { + values: HashMap::new(), + next_id: 1, + } + } + + pub fn add(&mut self, name: String, amount: f64, tx_type: TransactionType) -> Expense { + let current_id = self.next_id; + let new_expense = Expense { + id: current_id, + name, + amount, + tx_type, + }; + self.values.insert(current_id, new_expense.clone()); + self.next_id += 1; + new_expense + } + + pub fn view_all(&self) -> Vec<&Expense> { + self.values.values().collect() + } + + pub fn update(&mut self, id: u8, amount: f64, tx_type: TransactionType) -> Expense { + let updated_data = self.values.get_mut(&id).expect("Failed to Update"); + updated_data.amount = amount; + updated_data.tx_type = tx_type; + updated_data.clone() + } + + pub fn delete(&mut self, id: u8) -> bool { + self.values.remove(&id).is_some() + } + + pub fn save_to_file(&self, filename: &str) { + let json = serde_json::to_string_pretty(&self.values).expect("Failed to serialize"); + let mut file = File::create(filename).expect("Failed to create file"); + file.write_all(json.as_bytes()).expect("Failed to write"); + + println!("Data saved to {}", filename); + } + + pub fn load_from_file(filename: &str) -> HashMap { + if let Ok(mut file) = File::open(filename) { + println!("\n-------------------------------\n"); + println!("Loading data from {}", filename); + println!("\n-------------------------------\n"); + let mut content = String::new(); + file.read_to_string(&mut content).ok(); + println!("\n-------------------------------\n"); + println!("Data loaded from {}", filename); + println!("\n-------------------------------\n"); + serde_json::from_str(&content).unwrap_or_default() + } else { + HashMap::new() + } + } +}