-
-
Notifications
You must be signed in to change notification settings - Fork 103
WIP: Compute balances in database #247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 10 commits
a9ed276
beb89da
df6a7ad
ab4c7ee
56af112
de25193
24d1b99
70e58c6
a40ca8c
cee753c
23fece2
b3002be
20fb843
44a3bdd
a36344d
cd32056
c619e26
646c2e0
7de67eb
bb996e9
fa22c43
1ff2229
6ff9a98
0e5b29c
bec3f4d
af4c06d
2684a71
4b02518
c26dc4a
06eea78
bca6c9d
4dc3d38
1f330a5
3270d55
510df2e
391f161
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| src/prisma/client/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |
| # database | ||
| /prisma/db.sqlite | ||
| /prisma/db.sqlite-journal | ||
| /src/prisma/client | ||
|
|
||
| # next.js | ||
| /.next/ | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The original There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My plan was to only use this as and intermediate script and revert to the original before we merge. If you think it's worth keeping around I'll move it to a separate script. |
krokosik marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| -- @param {Int} $1:id of the group | ||
| select "groupId", "userId" as "borrowedBy", "paidBy", "currency", Coalesce(-1 * sum("ExpenseParticipant".amount),0) as amount FROM "Expense" | ||
| JOIN "ExpenseParticipant" ON "ExpenseParticipant"."expenseId" = "Expense".id | ||
| WhERE "groupId" = $1 AND "userId" != "paidBy" | ||
| GROUP BY "userId", "paidBy", "currency", "groupId" | ||
| ORDER By "currency" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| -- @param {Int} $1:id of the user | ||
| SELECT "Group"."id", "Group".name, Coalesce(sum("ExpenseParticipant".amount),0) as balance, Coalesce("Expense".currency, "Group"."defaultCurrency") as currency | ||
| FROM "GroupUser" | ||
| JOIN "Group" ON "GroupUser"."groupId" = "Group".id | ||
| LEFT JOIN "Expense" ON "Expense"."groupId" = "Group".id | ||
| LEFT JOIN "ExpenseParticipant" ON "Expense".id = "ExpenseParticipant"."expenseId" | ||
| WHERE "GroupUser"."userId" = $1 AND "deletedAt" IS NULL AND ("ExpenseParticipant"."userId" = $1 OR "Expense".id is null) | ||
| GROUP BY "Group".id, "Group".name, "Expense".currency | ||
| ORDER BY "Group"."createdAt" DESC, balance DESC |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,4 @@ | ||
| import { type GroupBalance } from '@prisma/client'; | ||
| import { addHours } from 'date-fns'; | ||
| import { type getAllBalancesForGroup } from '~/prisma/client/sql'; | ||
|
|
||
| import { simplifyDebts } from './simplify'; | ||
|
|
||
|
|
@@ -9,46 +8,45 @@ type MinimalEdge = { | |
| amount: bigint; | ||
| }; | ||
|
|
||
| const sortByIds = (a: GroupBalance, b: GroupBalance) => { | ||
| if (a.userId === b.userId) { | ||
| return a.firendId - b.firendId; | ||
| const sortByIds = (a: getAllBalancesForGroup.Result, b: getAllBalancesForGroup.Result) => { | ||
| if (a.paidBy === b.paidBy) { | ||
| return a.borrowedBy - b.borrowedBy; | ||
| } | ||
| return a.userId - b.userId; | ||
| return a.paidBy - b.paidBy; | ||
|
Comment on lines
+11
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer to keep this PR as minimal as possible. As such, please keep the property names identical and create a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to confirm, you want me to keep the existing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While I agree the naming even without the typo is quite unfortunate, I would prefer to make such a critical PR as small and easy to review as possible. We can think about another PR afterwards. |
||
| }; | ||
|
|
||
| let dateCounter = 0; | ||
|
|
||
| const edgeToGroupBalance = (edge: MinimalEdge): [GroupBalance, GroupBalance] => { | ||
| const edgeToGroupBalance = ( | ||
| edge: MinimalEdge, | ||
| ): [getAllBalancesForGroup.Result, getAllBalancesForGroup.Result] => { | ||
| const base = { | ||
| groupId: 0, | ||
| currency: 'USD', | ||
| updatedAt: addHours(new Date(), dateCounter++), | ||
| }; | ||
| return [ | ||
| { | ||
| userId: edge.userOne, | ||
| firendId: edge.userTwo, | ||
| paidBy: edge.userOne, | ||
| borrowedBy: edge.userTwo, | ||
| amount: edge.amount, | ||
| ...base, | ||
| }, | ||
| { | ||
| userId: edge.userTwo, | ||
| firendId: edge.userOne, | ||
| paidBy: edge.userTwo, | ||
| borrowedBy: edge.userOne, | ||
| amount: edge.amount === 0n ? 0n : -edge.amount, | ||
| ...base, | ||
| }, | ||
| ]; | ||
| }; | ||
|
|
||
| const padWithZeroBalances: (balances: GroupBalance[], userCount: number) => GroupBalance[] = ( | ||
| balances, | ||
| userCount, | ||
| ) => { | ||
| const padWithZeroBalances: ( | ||
| balances: getAllBalancesForGroup.Result[], | ||
| userCount: number, | ||
| ) => getAllBalancesForGroup.Result[] = (balances, userCount) => { | ||
| const result = [...balances]; | ||
| for (let userId = 0; userId < userCount; userId++) { | ||
| for (let friendId = userId + 1; friendId < userCount; friendId++) { | ||
| const found = balances.find( | ||
| (balance) => balance.userId === userId && balance.firendId === friendId, | ||
| (balance) => balance.paidBy === userId && balance.borrowedBy === friendId, | ||
| ); | ||
|
|
||
| if (!found) { | ||
|
|
@@ -59,14 +57,17 @@ const padWithZeroBalances: (balances: GroupBalance[], userCount: number) => Grou | |
| return result; | ||
| }; | ||
|
|
||
| const getFullBalanceGraph = (edges: MinimalEdge[], userCount: number): GroupBalance[] => { | ||
| const getFullBalanceGraph = ( | ||
| edges: MinimalEdge[], | ||
| userCount: number, | ||
| ): getAllBalancesForGroup.Result[] => { | ||
| const arr = padWithZeroBalances(edges.flatMap(edgeToGroupBalance), userCount); | ||
| arr.sort(sortByIds); | ||
| return arr; | ||
| }; | ||
|
|
||
| // taken from https://www.geeksforgeeks.org/minimize-cash-flow-among-given-set-friends-borrowed-money/ | ||
| const smallGraph: GroupBalance[] = getFullBalanceGraph( | ||
| const smallGraph: getAllBalancesForGroup.Result[] = getFullBalanceGraph( | ||
| [ | ||
| { userOne: 0, userTwo: 1, amount: 1000n }, | ||
| { userOne: 1, userTwo: 2, amount: 5000n }, | ||
|
|
@@ -75,19 +76,16 @@ const smallGraph: GroupBalance[] = getFullBalanceGraph( | |
| 3, | ||
| ); | ||
|
|
||
| const smallGraphResult: GroupBalance[] = getFullBalanceGraph( | ||
| const smallGraphResult: getAllBalancesForGroup.Result[] = getFullBalanceGraph( | ||
| [ | ||
| { userOne: 1, userTwo: 2, amount: 4000n }, | ||
| { userOne: 2, userTwo: 0, amount: -3000n }, | ||
| ], | ||
| 3, | ||
| ).map((resultBalance, idx) => ({ | ||
| ...resultBalance, | ||
| updatedAt: smallGraph[idx]!.updatedAt, | ||
| })); | ||
| ); | ||
|
|
||
| // taken from https://medium.com/@mithunmk93/algorithm-behind-splitwises-debt-simplification-feature-8ac485e97688 | ||
| const largeGraph: GroupBalance[] = getFullBalanceGraph( | ||
| const largeGraph: getAllBalancesForGroup.Result[] = getFullBalanceGraph( | ||
| [ | ||
| { userOne: 1, userTwo: 2, amount: 4000n }, | ||
| { userOne: 1, userTwo: 5, amount: -1000n }, | ||
|
|
@@ -105,7 +103,7 @@ const largeGraph: GroupBalance[] = getFullBalanceGraph( | |
| 7, | ||
| ); | ||
|
|
||
| const denseGraph: GroupBalance[] = getFullBalanceGraph( | ||
| const denseGraph: getAllBalancesForGroup.Result[] = getFullBalanceGraph( | ||
| [ | ||
| { userOne: 0, userTwo: 1, amount: 895795n }, | ||
| { userOne: 0, userTwo: 2, amount: 328043n }, | ||
|
|
@@ -142,23 +140,25 @@ describe('simplifyDebts', () => { | |
| { graph: largeGraph, expected: 3 }, | ||
| { graph: denseGraph, expected: 5 }, | ||
| ])('gets the optimal operation count', ({ graph, expected }) => { | ||
| expect(simplifyDebts(graph).filter((balance) => balance.amount > 0).length).toBe(expected); | ||
| expect(simplifyDebts(graph).filter((balance) => (balance.amount ?? 0n) > 0).length).toBe( | ||
| expected, | ||
| ); | ||
| }); | ||
|
|
||
| it.each([{ graph: smallGraph }, { graph: largeGraph }, { graph: denseGraph }])( | ||
| 'preserves the total balance per user', | ||
| ({ graph }) => { | ||
| const startingBalances = graph.reduce( | ||
| (acc, balance) => { | ||
| acc[balance.userId] = (acc[balance.userId] ?? 0n) + balance.amount; | ||
| acc[balance.paidBy] = (acc[balance.paidBy] ?? 0n) + (balance.amount ?? 0n); | ||
| return acc; | ||
| }, | ||
| {} as Record<number, bigint>, | ||
| ); | ||
|
|
||
| const userBalances = simplifyDebts(graph).reduce( | ||
| (acc, balance) => { | ||
| acc[balance.userId] = (acc[balance.userId] ?? 0n) + balance.amount; | ||
| acc[balance.paidBy] = (acc[balance.paidBy] ?? 0n) + (balance.amount ?? 0n); | ||
| return acc; | ||
| }, | ||
| {} as Record<number, bigint>, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.