"""Utility functions for the Finance Manager application.""" import pandas as pd import os from datetime import datetime from typing import Optional class CSVLedger: """Handles CSV persistence for the expense ledger.""" def __init__(self, filepath: str = "ledger.csv"): """ Initialize the CSV ledger handler. Args: filepath: Path to the CSV file """ self.filepath = filepath self.df = self._load_or_create() def _load_or_create(self) -> pd.DataFrame: """Load existing CSV or create new DataFrame.""" if os.path.exists(self.filepath): try: df = pd.read_csv(self.filepath) df["Date"] = pd.to_datetime(df["Date"]) df["Amount"] = pd.to_numeric(df["Amount"]) return df.sort_values("Date", ascending=False).reset_index(drop=True) except Exception as e: print(f"Error loading CSV: {e}. Creating new ledger.") return pd.DataFrame(columns=["Date", "Description", "Category", "Amount"]) def save(self, df: pd.DataFrame) -> bool: """ Save DataFrame to CSV. Args: df: DataFrame to save Returns: True if successful, False otherwise """ try: # Convert datetime to string for CSV df_copy = df.copy() df_copy["Date"] = df_copy["Date"].dt.strftime("%Y-%m-%d") df_copy.to_csv(self.filepath, index=False) return True except Exception as e: print(f"Error saving CSV: {e}") return False def append_from_dataframe(self, df: pd.DataFrame) -> bool: """ Append DataFrame entries to CSV. Args: df: DataFrame with new entries Returns: True if successful, False otherwise """ self.df = pd.concat([self.df, df], ignore_index=True) self.df = self.df.sort_values("Date", ascending=False).reset_index(drop=True) return self.save(self.df) def format_currency(amount: float) -> str: """ Format amount as USD currency. Args: amount: Numeric amount Returns: Formatted string like "$123.45" """ return f"${amount:,.2f}" def parse_date_flexible(date_str: Optional[str]) -> str: """ Parse various date formats and return ISO format (YYYY-MM-DD). Args: date_str: Date string in various formats or None Returns: ISO format date string """ if not date_str or date_str.lower() == "today" or date_str.lower() == "now": return datetime.now().strftime("%Y-%m-%d") # Try common formats formats = [ "%Y-%m-%d", "%m/%d/%Y", "%m/%d/%y", "%m-%d-%Y", "%d/%m/%Y", "%Y/%m/%d", ] for fmt in formats: try: dt = datetime.strptime(date_str.strip(), fmt) return dt.strftime("%Y-%m-%d") except ValueError: continue # Default to today return datetime.now().strftime("%Y-%m-%d") def get_spending_summary(df: pd.DataFrame) -> dict: """ Generate spending summary by category. Args: df: Expense DataFrame Returns: Dictionary with category totals """ if df.empty: return {} summary = df.groupby("Category")["Amount"].agg(["sum", "count"]).to_dict("index") return { cat: { "total": values["sum"], "count": int(values["count"]), "average": values["sum"] / values["count"] } for cat, values in summary.items() } def get_daily_summary(df: pd.DataFrame) -> pd.DataFrame: """ Generate daily spending summary. Args: df: Expense DataFrame Returns: DataFrame with daily totals """ if df.empty: return pd.DataFrame(columns=["Date", "Total", "Count"]) daily = df.groupby(df["Date"].dt.date).agg({ "Amount": ["sum", "count"] }).reset_index() daily.columns = ["Date", "Total", "Count"] return daily.sort_values("Date", ascending=False) def validate_expense_data(date: str, description: str, category: str, amount: float) -> tuple[bool, str]: """ Validate expense entry data. Args: date: Date string description: Expense description category: Expense category amount: Amount in dollars Returns: Tuple of (is_valid, error_message) """ errors = [] # Validate date if not date: errors.append("Date is required") else: try: datetime.strptime(date, "%Y-%m-%d") except ValueError: errors.append("Date must be in YYYY-MM-DD format") # Validate description if not description or len(description.strip()) == 0: errors.append("Description is required") elif len(description) > 500: errors.append("Description is too long (max 500 characters)") # Validate category if not category or len(category.strip()) == 0: errors.append("Category is required") # Validate amount if amount is None or amount <= 0: errors.append("Amount must be greater than 0") elif amount > 999999.99: errors.append("Amount is too large (max $999,999.99)") if errors: return False, "\n".join(errors) return True, "" def export_to_csv(df: pd.DataFrame, filepath: str) -> bool: """ Export DataFrame to CSV file. Args: df: DataFrame to export filepath: Output file path Returns: True if successful, False otherwise """ try: df_copy = df.copy() df_copy["Date"] = df_copy["Date"].dt.strftime("%Y-%m-%d") df_copy.to_csv(filepath, index=False) return True except Exception as e: print(f"Error exporting to CSV: {e}") return False