""" ================================================================================ Project 02: Expense Tracker Difficulty: 🟡 Intermediate ================================================================================ A command-line expense tracker that lets users add, view, filter, and analyze their expenses. Data is persisted to a JSON file between sessions. Concepts Used: - Dictionaries and Lists - File I/O (JSON) - Functions with parameters - String formatting - datetime module - Data filtering and aggregation ================================================================================ """ import json import os from datetime import datetime # ----------------------------------------------------------------------------- # Constants # ----------------------------------------------------------------------------- DATA_FILE = os.path.join(os.path.dirname(__file__), "expenses.json") CATEGORIES = { "food": "🍔", "transport": "🚗", "entertainment": "🎬", "bills": "📄", "other": "📦", } # ----------------------------------------------------------------------------- # Data Management # ----------------------------------------------------------------------------- def load_expenses() -> list: """Load expenses from the JSON file.""" if os.path.exists(DATA_FILE): with open(DATA_FILE, "r") as f: return json.load(f) return [] def save_expenses(expenses: list) -> None: """Save expenses to the JSON file.""" with open(DATA_FILE, "w") as f: json.dump(expenses, f, indent=2) def generate_id(expenses: list) -> int: """Generate a unique ID for a new expense.""" if not expenses: return 1 return max(exp["id"] for exp in expenses) + 1 # ----------------------------------------------------------------------------- # Core Features # ----------------------------------------------------------------------------- def add_expense(expenses: list) -> None: """Add a new expense.""" description = input(" Enter description: ").strip() if not description: print(" ❌ Description cannot be empty.") return try: amount = float(input(" Enter amount: $")) if amount <= 0: print(" ❌ Amount must be positive.") return except ValueError: print(" ❌ Invalid amount.") return print(f" Categories: {', '.join(CATEGORIES.keys())}") category = input(" Enter category: ").strip().lower() if category not in CATEGORIES: print(f" ⚠️ Unknown category. Defaulting to 'other'.") category = "other" expense = { "id": generate_id(expenses), "description": description, "amount": amount, "category": category, "date": datetime.now().strftime("%Y-%m-%d %H:%M"), } expenses.append(expense) save_expenses(expenses) print(f" ✅ Expense added: {description} — ${amount:.2f} [{category}]") def view_expenses(expenses: list, filter_category: str = None) -> None: """Display expenses in a formatted table.""" filtered = expenses if filter_category: filtered = [e for e in expenses if e["category"] == filter_category] if not filtered: print(" 📭 No expenses found.") return # Header print(f"\n {'ID':<4} {'Date':<18} {'Category':<15} {'Description':<20} {'Amount':>10}") print(f" {'—'*4} {'—'*18} {'—'*15} {'—'*20} {'—'*10}") # Rows for exp in filtered: icon = CATEGORIES.get(exp["category"], "📦") print( f" {exp['id']:<4} " f"{exp['date']:<18} " f"{icon} {exp['category']:<12} " f"{exp['description']:<20} " f"${exp['amount']:>8.2f}" ) total = sum(e["amount"] for e in filtered) print(f"\n {'Total:':<60} ${total:>8.2f}") def view_by_category(expenses: list) -> None: """Filter and view expenses by category.""" print(f" Categories: {', '.join(CATEGORIES.keys())}") category = input(" Enter category to filter: ").strip().lower() if category not in CATEGORIES: print(" ❌ Invalid category.") return view_expenses(expenses, filter_category=category) def show_summary(expenses: list) -> None: """Display spending summary with category breakdown.""" if not expenses: print(" 📭 No expenses to summarize.") return total = sum(e["amount"] for e in expenses) # Group by category by_category = {} for exp in expenses: cat = exp["category"] by_category[cat] = by_category.get(cat, 0) + exp["amount"] print(f"\n 📊 EXPENSE SUMMARY") print(f" {'='*40}") print(f" Total Expenses: ${total:,.2f}") print(f" Total Items: {len(expenses)}") print(f" {'—'*40}") # Sort categories by amount (descending) sorted_cats = sorted(by_category.items(), key=lambda x: x[1], reverse=True) for cat, amount in sorted_cats: icon = CATEGORIES.get(cat, "📦") percentage = (amount / total) * 100 bar = "█" * int(percentage / 5) print(f" {icon} {cat:<15} ${amount:>8.2f} ({percentage:>5.1f}%) {bar}") def delete_expense(expenses: list) -> None: """Delete an expense by its ID.""" view_expenses(expenses) if not expenses: return try: exp_id = int(input("\n Enter expense ID to delete: ")) except ValueError: print(" ❌ Invalid ID.") return for i, exp in enumerate(expenses): if exp["id"] == exp_id: removed = expenses.pop(i) save_expenses(expenses) print(f" 🗑️ Deleted: {removed['description']} — ${removed['amount']:.2f}") return print(f" ❌ Expense with ID {exp_id} not found.") # ----------------------------------------------------------------------------- # Main Menu # ----------------------------------------------------------------------------- def display_menu(): """Display the main menu.""" print(""" ╔══════════════════════════════════════════╗ ║ 💰 EXPENSE TRACKER 💰 ║ ╠══════════════════════════════════════════╣ ║ 1. Add Expense ║ ║ 2. View All Expenses ║ ║ 3. View by Category ║ ║ 4. Summary ║ ║ 5. Delete Expense ║ ║ 6. Exit ║ ╚══════════════════════════════════════════╝ """) def main(): """Main entry point.""" expenses = load_expenses() while True: display_menu() choice = input(" Choose an option (1-6): ").strip() if choice == "1": add_expense(expenses) elif choice == "2": view_expenses(expenses) elif choice == "3": view_by_category(expenses) elif choice == "4": show_summary(expenses) elif choice == "5": delete_expense(expenses) elif choice == "6": print("\n 👋 Goodbye! Keep tracking your expenses!") break else: print(" ❌ Invalid option. Please choose 1-6.") input("\n Press Enter to continue...") if __name__ == "__main__": main()