File size: 8,128 Bytes
39a23e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3c822c9
0f92cf6
 
3c822c9
 
0f92cf6
39a23e4
 
 
 
 
 
3c822c9
39a23e4
 
3c822c9
0f92cf6
 
3c822c9
 
0f92cf6
39a23e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70235da
39a23e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3c822c9
39a23e4
 
 
 
 
 
 
 
0f92cf6
 
3c822c9
0f92cf6
3c822c9
 
0f92cf6
39a23e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3c822c9
39a23e4
 
 
 
 
 
 
 
0f92cf6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
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)