Homework 4

There are many possible solutions, here are some of mine:

import os
from csv import DictReader, DictWriter


# This is some example data, so you can see the format
# and the data structures (list of dicts)
EXAMPLE_FINANCES = [
    {"date": "2025-02-17", "amount": "500", "category": "Work Study"},
    {"date": "2025-02-17", "amount": "-200", "category": "Senior Gala"},
    {"date": "2025-02-18", "amount": "-15.5", "category": "JJ's"},
    {"date": "2025-02-18", "amount": "50", "category": "Dining Dollars"},
    {"date": "2025-02-19", "amount": "-5", "category": "Joe Coffee"},
    {"date": "2025-02-20", "amount": "-50.0", "category": "Music Hum Performance"},
    {"date": "2025-02-21", "amount": "-5", "category": "Joe Coffee"},
]


#####################
# UTILITY FUNCTIONS #
#####################
def write_finances_csv(file_path, data):
    """This is a helper function to write the list
    of dictionaries in the format above to a file"""
    file = open(file_path, 'w')
    csvwriter = DictWriter(file, fieldnames=["date", "amount", "category"])
    for row in data:
        csvwriter.writerow(row)
    file.close()


def read_finances_csv(file_path):
    """This is a helper function to read data in the format
    above from a file"""
    if not os.path.exists(file_path):
        return EXAMPLE_FINANCES
    file = open(file_path, 'r')
    data = []
    for row in DictReader(file, fieldnames=["date", "amount", "category"]):
        data.append(row)
    file.close()
    return data

#####################
# COMMAND FUNCTIONS #
#####################
def print_all_entries(data):
    """This function takes a list of dictionaries and prints out
    all of the entries with some nice spacing and formatting"""

    print("\nAll entries:")

    for i, entry in enumerate(data):
        date = entry["date"]
        amount = entry["amount"]
        category = entry["category"]
        print(f"\t{i}: {date}: ${amount}\t{category}")

    # return data without changes
    return data

def reset_to_examples(data):
    """This function ignores `data` and instead 
    returns the example data from above"""
    return EXAMPLE_FINANCES

def clear_entries(data):
    """This data removes all entries from data. We can 'cheat'
    and just ignore `data` and return an empty list instead"""
    return []

def add_entry(data):
    """This function will prompt the user for date, amount, and category,
    and append the new dictionary entry to the end of the list"""
    date = input("Enter the date: ")
    amount = input("Enter the amount: ")
    category = input("Enter the category: ")
    data.append({"date": date, "amount": amount, "category": category})
    return data

def delete_entry(data):
    """This function will show the entries to the user and then 
    prompt them for the index of the entry they want to delete"""
    """delete an entry"""
    # First print all the entries to remind the user
    print_all_entries(data)
    
    # Now ask for an entry
    index = int(input("Enter the index of the entry to delete, or -1 to cancel: "))
    
    if index >= 0:
        # remove it from the list
        data.pop(index)
    return data

def calculate_net_total(data):
    """This function will calculate the net total of the data"""
    total = 0
    for entry in data:
        total += float(entry["amount"])
    print(f"Net total: {total}")
    return data

def calculate_number_of_credits(data):
    """This function will calculate the number of credits in the data"""
    credits = 0
    for entry in data:
        amount = float(entry["amount"])
        if amount > 0:
            credits += 1
    print(f"Number of credits: {credits}")
    return data

def calculate_sum_of_credits(data):
    """This function will calculate the sum of credits in the data"""
    total_credits = 0
    for entry in data:
        amount = float(entry["amount"])
        if amount > 0:
            total_credits += amount
    print(f"Sum of credits: {total_credits}")
    return data

def calculate_number_of_debits(data):
    """This function will calculate the number of debits in the data"""
    debits = 0
    for entry in data:
        amount = float(entry["amount"])
        if amount < 0:
            debits += 1
    print(f"Number of debits: {debits}")
    return data

def calculate_sum_of_debits(data):
    """This function will calculate the sum of debits in the data"""
    total_debits = 0
    for entry in data:
        amount = float(entry["amount"])
        if amount < 0:
            total_debits += amount
    print(f"Sum of debits: {total_debits}")
    return data

def calculate_max_credit(data):
    """This function will calculate the maximum credit in the data"""
    max_credit = 0
    for entry in data:
        amount = float(entry["amount"])
        if amount > max_credit:
            max_credit = amount
    print(f"Max credit: {max_credit}")
    return data

def calculate_max_debit(data):
    """This function will calculate the maximum debit in the data"""
    max_debit = 0
    for entry in data:
        amount = float(entry["amount"])
        if amount < max_debit:
            max_debit = amount
    print(f"Max debit: {max_debit}")
    return data

def calculate_average_credit(data):
    """This function will calculate the average credit in the data"""
    total_credits = 0
    num_credits = 0
    for entry in data:
        amount = float(entry["amount"])
        if amount > 0:
            total_credits += amount
            num_credits += 1
    if num_credits == 0:
        print("Average credit: 0")
    else:
        print(f"Average credit: {total_credits / num_credits}")
    return data
    
def calculate_average_debit(data):
    """This function will calculate the average debit in the data"""
    total_debits = 0
    num_debits = 0
    for entry in data:
        amount = float(entry["amount"])
        if amount < 0:
            total_debits += amount
            num_debits += 1
    if num_debits == 0:
        print("Average debit: 0")
    else:
        print(f"Average debit: {total_debits / num_debits}")
    return data

def calculate_most_common_credit(data):
    """This function will calculate the most common credit in the data"""
    credit_counts = {}
    for entry in data:
        amount = float(entry["amount"])
        if amount > 0:
            credit_counts[amount] = credit_counts.get(amount, 0) + 1
    if not credit_counts:
        print("No credits")
    else:
        print(f"Most common credit: {max(credit_counts, key=credit_counts.get)}")
    return data

def calculate_most_common_debit(data):
    """This function will calculate the most common debit in the data"""
    debit_counts = {}
    for entry in data:
        amount = float(entry["amount"])
        if amount < 0:
            debit_counts[amount] = debit_counts.get(amount, 0) + 1
    if not debit_counts:
        print("No debits")
    else:
        print(f"Most common debit: {max(debit_counts, key=debit_counts.get)}")
    return data


def selection_sort_by_key(data, key):
    """This function will sort the data by the key, ascending"""
    for i in range(len(data)):
        min_index = i
        for j in range(i+1, len(data)):
            if data[j][key] < data[min_index][key]:
                min_index = j
        data[i], data[min_index] = data[min_index], data[i]
    return data

def sort_entries_by_date(data):
    ascending_or_desceding = input("Enter 'asc' for ascending or 'desc' for descending: ")
    data = selection_sort_by_key(data, "date")
    if ascending_or_desceding == "desc":
        data.reverse()
    return data

def sort_entries_by_amount(data):
    ascending_or_desceding = input("Enter 'asc' for ascending or 'desc' for descending: ")
    data = selection_sort_by_key(data, "amount")
    if ascending_or_desceding == "desc":
        data.reverse()
    return data

def sort_entries_by_category(data):
    ascending_or_desceding = input("Enter 'asc' for ascending or 'desc' for descending: ")
    data = selection_sort_by_key(data, "category")
    if ascending_or_desceding == "desc":
        data.reverse()
    return data

def filter_entries_by_date(data):
    """This function will filter the data by a date"""
    date = input("Enter the date to filter by: ")
    new_data = []
    for entry in data:
        if entry["date"] == date:
            new_data.append(entry)
    return new_data

def filter_entries_by_amount(data):
    """This function will filter the data by an amount"""
    amount = input("Enter the amount to filter by: ")
    gt_or_lt = input("Enter 'gt' for greater than or 'lt' for less than: ")

    new_data = []
    for entry in data:
        if gt_or_lt == "gt" and float(entry["amount"]) > float(amount):
            new_data.append(entry)
        elif gt_or_lt == "lt" and float(entry["amount"]) < float(amount):
            new_data.append(entry)
    return new_data

def filter_entries_by_category(data):
    """This function will filter the data by a category"""
    category = input("Enter the category to filter by: ")
    new_data = []
    for entry in data:
        if entry["category"] == category:
            new_data.append(entry)
    return new_data


def modify_existing_entry(data):
    """This function will show the entries to the user and then 
    prompt them for the index of the entry they want to modify"""
    print_all_entries(data)
    index = int(input("Enter the index of the entry to modify, or -1 to cancel: "))
    if index >= 0:
        date = input("Enter the date: ")
        amount = input("Enter the amount: ")
        category = input("Enter the category: ")
        data[index] = {"date": date, "amount": amount, "category": category}
    return data

def remove_duplicate_entries(data):
    """This function will remove duplicate entries from the data"""
    seen = set()
    new_data = []
    for entry in data:
        key = (entry["date"], entry["amount"], entry["category"])
        if key not in seen:
            seen.add(key)
            new_data.append(entry)
    return new_data

def remove_invalid_entries(data):
    """This function will remove invalid entries from the data"""
    new_data = []
    for entry in data:
        date = entry["date"]
        amount = entry["amount"]
        category = entry["category"]
        try:
            float(amount)
        except ValueError:
            continue
        if not date or not category:
            continue
        new_data.append(entry)
    return new_data
    
#################
# MAIN FUNCTION #
#################
def main():
    """This is our main function. It is the primary entry point into our program
    and it does a couple of things. See the inline comments for details"""
    

    # Our list of commands.
    # This is a dictionary mapping a command (in our case an integer)
    # to the description of the command and the function. All of our
    # commands take the the form:
    #
    #     new_data = function(current_data)
    #
    # So they will take the current list of expenses and return a (maybe different)
    # list of expenses.
    #
    # You will add new commands here as you implement them
    commands = {
        "0": ["Save and quit", None],
        "1": ["Print all entries", print_all_entries],
        "2": ["Reset to examples", reset_to_examples],
        "3": ["Clear all entries", clear_entries],
        "4": ["Add new entry", add_entry],
        "5": ["Delete an entry", delete_entry],
        # YOUR NEW COMMANDS HERE
        "6": ["Calculate number of credits", calculate_number_of_credits],
        "7": ["Calculate sum of credits", calculate_sum_of_credits],
        "8": ["Calculate number of debits", calculate_number_of_debits],
        "9": ["Calculate sum of debits", calculate_sum_of_debits],
        "10": ["Calculate max credit", calculate_max_credit],
        "11": ["Calculate max debit", calculate_max_debit],
        "12": ["Calculate average credit", calculate_average_credit],
        "13": ["Calculate average debit", calculate_average_debit],
        "14": ["Calculate most common credit", calculate_most_common_credit],
        "15": ["Calculate most common debit", calculate_most_common_debit],
        "16": ["Sort entries by date", sort_entries_by_date],
        "17": ["Sort entries by amount", sort_entries_by_amount],
        "18": ["Sort entries by category", sort_entries_by_category],
        "19": ["Filter entries by date", filter_entries_by_date],
        "20": ["Filter entries by amount", filter_entries_by_amount],
        "21": ["Filter entries by category", filter_entries_by_category],
        "22": ["Modify existing entry", modify_existing_entry],
        "23": ["Remove duplicate entries", remove_duplicate_entries],
        "24": ["Remove invalid entries", remove_invalid_entries],
        "25": ["Calculate net total", calculate_net_total],
    }

    # This is the file we will work from
    finances_csv_filename = "finances.csv"
    
    # Load up the data
    data = read_finances_csv(finances_csv_filename)
    
    # Print a welcome message
    print("Welcome to the finances app!")
    
    # Enter an infinite loop until the user
    # chooses to quit
    while True:
        # Print the available commands
        print("\nPick a command to continue:")
        for command_number in commands:
            description, function = commands[command_number]
            print(f"\t{command_number}.  {description}")
    
        # Get the user's choice
        choice = input("Enter a command number: ")
        
        # If the choice is not valid, print an error message
        if choice not in commands:
            print("Invalid choice. Please try again.")
            continue
        
        # Handle saving and quitting
        if choice == "0":
            break
        
        # Otherwise, call the function associated with the choice
        # Pass in the data and return the new/modified data
        data = commands[choice][1](data)
        
    # Finally, save the changes
    write_finances_csv(finances_csv_filename, data)
    
   
    
if __name__ == "__main__":
    main()