expense-wizard / app.py
hlydecker's picture
Update app.py
70235da verified
import gradio as gr
import pandas as pd
import numpy as np
# Global variables
expenses = []
participants_set = set()
def add_participant(participant):
global participants_set
if not participant or not participant.strip():
raise gr.Error("Participant name cannot be empty")
clean_name = participant.strip()
participants_set.add(clean_name)
participants_list = sorted(list(participants_set)) # Sort for consistent display
participants_text = "\n".join(participants_list)
# Return updated participant list and new Dropdown instances
return (
participants_text,
gr.Dropdown(choices=participants_list, label="Payer", interactive=True),
gr.Dropdown(choices=participants_list, label="Participants", multiselect=True, interactive=True)
)
def remove_participant(participant):
global participants_set
participant = participant.strip()
if participant in participants_set:
participants_set.remove(participant)
participants_list = sorted(list(participants_set)) # Sort for consistent display
participants_text = "\n".join(participants_list)
# Return updated participant list and new Dropdown instances
return (
participants_text,
gr.Dropdown(choices=participants_list, label="Payer", interactive=True),
gr.Dropdown(choices=participants_list, label="Participants", multiselect=True, interactive=True)
)
# Add expenses
def add_expense(description, amount, payer, participants_list):
global expenses
# Validate inputs
if not description or not description.strip():
raise gr.Error("Description cannot be empty")
if amount <= 0:
raise gr.Error("Amount must be a positive number")
if not payer:
raise gr.Error("Payer cannot be empty")
if not participants_list:
raise gr.Error("Participants cannot be empty")
# Ensure all are unique
unique_participants = list(set(participants_list + [payer]))
amount = float(amount)
expense = {
"Description": description,
"Amount": amount,
"Payer": payer,
"Participants": ", ".join(unique_participants),
"Split Amount": round(amount / len(unique_participants), 2),
}
expenses.append(expense)
return pd.DataFrame(expenses)
# Optimize Balances
def optimize_balances():
global expenses
# Create a comprehensive balance sheet
balances = {}
for expense in expenses:
payer = expense["Payer"]
participants_list = expense["Participants"].split(", ")
total_amount = expense["Amount"]
split_amount = total_amount / len(participants_list)
# Payer gets credit for the entire amount
balances[payer] = balances.get(payer, 0) + total_amount
# Participants owe their share
for participant in participants_list:
if participant != payer:
balances[participant] = balances.get(participant, 0) - split_amount
# Simplify debts
def simplify_debts(balances):
# Round balances to avoid floating-point errors
rounded_balances = {k: round(v, 2) for k, v in balances.items()}
# Separate creditors and debtors
debtors = {k: v for k, v in rounded_balances.items() if v < -0.01}
creditors = {k: v for k, v in rounded_balances.items() if v > 0.01}
transactions = []
# Continue until all debts are settled
while debtors and creditors:
# Find the most negative debtor and the largest creditor
debtor = min(debtors, key=debtors.get)
creditor = max(creditors, key=creditors.get)
# Amount to settle is the minimum of absolute debt and credit
settle_amount = min(abs(debtors[debtor]), creditors[creditor])
# Round to 2 decimal places
settle_amount = round(settle_amount, 2)
# Add transaction
transactions.append(f"{debtor} pays {creditor} ${settle_amount:.2f}")
# Update balances
debtors[debtor] += settle_amount
creditors[creditor] -= settle_amount
# Remove if balance is effectively zero
if abs(debtors[debtor]) < 0.01:
del debtors[debtor]
if abs(creditors[creditor]) < 0.01:
del creditors[creditor]
return transactions if transactions else ["No transactions needed"]
return "\n".join(simplify_debts(balances))
# Reset App
def reset():
global expenses, participants_set
expenses = []
participants_set = set()
participants_list = []
participants_text = ""
return (
pd.DataFrame(expenses),
"",
participants_text,
gr.Dropdown(choices=participants_list, label="Payer", interactive=True),
gr.Dropdown(choices=participants_list, label="Participants", multiselect=True, interactive=True)
)
# Gradio Interface
with gr.Blocks(theme='soft') as app:
gr.Markdown("# Expense Splitter App")
# Participant Management
with gr.Row():
with gr.Column():
participant_input = gr.Textbox(label="Participant Name", placeholder="Enter a participant name")
with gr.Row():
add_participant_btn = gr.Button("Add Participant")
remove_participant_btn = gr.Button("Remove Participant")
participants_display = gr.Textbox(
label="Current Participants",
lines=10,
interactive=False,
placeholder="Participants will appear here..."
)
# Expense Adding
with gr.Row():
with gr.Column():
description = gr.Textbox(label="Description", placeholder="e.g., Dinner")
amount = gr.Number(label="Amount", value=0, precision=2)
payer = gr.Dropdown(
label="Payer",
choices=[],
interactive=True
)
participants = gr.Dropdown(
label="Participants",
multiselect=True,
choices=[],
interactive=True
)
add_btn = gr.Button("Add Expense")
with gr.Column():
expense_table = gr.Dataframe(
headers=["Description", "Amount", "Payer", "Participants", "Split Amount"],
datatype=["str", "number", "str", "str", "number"],
type="pandas"
)
# Button Interactions
add_participant_btn.click(
add_participant,
inputs=participant_input,
outputs=[participants_display, payer, participants]
)
remove_participant_btn.click(
remove_participant,
inputs=participant_input,
outputs=[participants_display, payer, participants]
)
add_btn.click(
add_expense,
inputs=[description, amount, payer, participants],
outputs=expense_table
)
with gr.Row():
optimize_btn = gr.Button("Optimize Balances")
result = gr.Textbox(label="Transactions", lines=5)
reset_btn = gr.Button("Reset")
optimize_btn.click(optimize_balances, inputs=[], outputs=result)
reset_btn.click(
reset,
inputs=[],
outputs=[expense_table, result, participants_display, payer, participants]
)
# Launch the app
if __name__ == "__main__":
app.launch(share=True)