Spaces:
Build error
Build error
| """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 | |