Problemshared trip expense ledger

Session: Sign in to solve

Solution.txt

Shared Trip Expense Ledger

Problem

A travel group needs balances that remain explainable after expenses are settled or corrected. Design TripExpenseLedger with fixed membership, an immutable journal of expenses and settlements, compensating reversals, and current member balances. Calls on one instance are sequential.

Requirements

  • Fixed membership. Construction accepts 2 to 100 unique member IDs that match [A-Za-z0-9_-]{1,40}. Invalid construction raises the runtime's standard invalid-argument exception, and membership never changes.
  • Immutable journal. Each accepted expense, settlement, or reversal appends one entry with a unique entry ID. Existing entries are never edited or removed, and rejected operations append nothing and do not reserve the proposed entry ID.
  • Expense splits. An expense credits its payer by the full amount and debits every participant by its share. EQUAL divides by sorted participant ID and assigns one remainder cent to each earliest participant; EXACT preserves the input pairing between participants and positive shares.
  • Balanced postings. Posting amounts within each entry sum to zero. A member balance is the sum of that member's postings across the journal, and all member balances also sum to zero.
  • Controlled settlements. A settlement moves value from a member with a negative balance to one with a positive balance. It cannot move either balance through zero and does not depend on a stored pairwise debt.
  • Compensating reversals. Reversing an expense or settlement appends its exact negated postings. A target may be reversed once, and a reversal cannot itself be reversed.
  • Failure atomicity. Validation uses the stated precedence. A rejected call changes no balance, journal entry, or reversal state.
  • Stable snapshots. Balance results contain every member sorted by memberId. Entry results follow successful append order, with nonzero postings sorted by memberId. Returned arrays and view objects are detached from ledger state.
  • Exact arithmetic. Money and balances use signed 64-bit integer cents. Floating-point arithmetic is not part of the contract.

API

SignatureReturnsBehavior
TripExpenseLedger(memberIds: string[])Not applicableCreates an empty ledger after validating fixed membership. Invalid input raises Python ValueError, Java IllegalArgumentException, or C++ std::invalid_argument.
recordExpense(entryId: string, payerId: string, amountCents: int64, participantIds: string[], splitMode: string, splitValues: int64[])stringReturns an expense status. On OK, appends one EXPENSE entry.
recordSettlement(entryId: string, debtorId: string, creditorId: string, amountCents: int64)stringReturns a settlement status. On OK, posts +amountCents to the debtor and -amountCents to the creditor.
reverseEntry(entryId: string, targetEntryId: string)stringReturns a reversal status. On OK, appends one REVERSAL entry and marks its target as reversed.
getBalances()BalanceView[]Returns a detached, sorted snapshot of every member's current balance.
getEntries()LedgerEntryView[]Returns a detached snapshot of the journal in append order.

BalanceView has fields memberId: string and balanceCents: int64.

LedgerEntryView has fields entryId: string, kind: string, amountCents: int64, primaryMemberId: string, secondaryMemberId: string, splitMode: string, reversalOfEntryId: string, and postings: BalanceView[].

Member and entry IDs are case-sensitive. Entry IDs use the same pattern as member IDs. All arguments are non-null, and input arrays must not be retained or modified.

Expense validation checks entry ID format, entry ID uniqueness, membership, amount, then split rules. It returns the first applicable status:

StatusCondition
OKThe expense was appended.
INVALID_ENTRY_IDentryId does not match [A-Za-z0-9_-]{1,40}.
DUPLICATE_ENTRYentryId belongs to any existing journal entry.
UNKNOWN_MEMBERThe payer or a participant is not a group member.
INVALID_AMOUNTamountCents is outside 1 through 1_000_000_000_000.
INVALID_SPLITA split rule is violated.

The participant list must be nonempty, contain no duplicates, and contain no more members than the group. The payer may be present or absent. For EQUAL, splitValues must be empty and the amount must be at least the participant count. If division gives quotient q and remainder r, each participant owes q, and the first r sorted participant IDs owe one additional cent. For EXACT, splitValues must have the same length as participantIds; each value is the positive share for the participant at the same index, and all shares must sum to the amount. Any other split mode is invalid. The payer credit and participant debit are combined when they name the same member, and zero net postings are omitted.

Settlement validation checks entry ID format, entry ID uniqueness, membership, amount, then current eligibility. It returns:

StatusCondition
OKThe settlement was appended.
INVALID_ENTRY_IDentryId violates the entry ID rule.
DUPLICATE_ENTRYentryId already exists.
UNKNOWN_MEMBERThe debtor or creditor is not a member.
INVALID_AMOUNTamountCents is outside 1 through 1_000_000_000_000.
INVALID_SETTLEMENTThe members are equal, the debtor balance is not negative, the creditor balance is not positive, or the amount exceeds either the debt or the credit.

Reversal validation checks entry ID format, entry ID uniqueness, then target eligibility. It returns:

StatusCondition
OKThe reversal was appended.
INVALID_ENTRY_IDThe proposed reversal ID violates the entry ID rule.
DUPLICATE_ENTRYThe proposed reversal ID already exists.
INVALID_REVERSALThe target is missing, is a reversal, or was already reversed.

An expense entry stores its payer as primaryMemberId, its mode as splitMode, and empty strings for secondaryMemberId and reversalOfEntryId. A settlement stores its debtor and creditor as the primary and secondary members. A reversal stores only its target in reversalOfEntryId. Fields not named for a kind are empty strings. A reversal's amountCents equals its target amount.

Examples

Starting with TripExpenseLedger(["cara", "alice", "bob"]):

StepOperationResult
1recordExpense("e1", "alice", 1000, ["cara", "alice", "bob"], "EQUAL", [])OK
2getBalances()[{"memberId":"alice","balanceCents":666},{"memberId":"bob","balanceCents":-333},{"memberId":"cara","balanceCents":-333}]
3recordSettlement("s1", "cara", "alice", 200)OK
4getBalances()[{"memberId":"alice","balanceCents":466},{"memberId":"bob","balanceCents":-333},{"memberId":"cara","balanceCents":-133}]
5recordSettlement("s2", "bob", "alice", 400)INVALID_SETTLEMENT; state is unchanged.
6reverseEntry("r1", "s1")OK
7getBalances()[{"memberId":"alice","balanceCents":666},{"memberId":"bob","balanceCents":-333},{"memberId":"cara","balanceCents":-333}]

In step 1, sorted participants receive shares alice=334, bob=333, and cara=333. Combining Alice's credit and share produces her +666 posting.

Constraints

  • Group size is 2 to 100 members.
  • At most 10,000 mutating calls and 10,000 query calls are made per ledger.
  • Expense and settlement amounts are 1 through 1_000_000_000_000 cents.
  • All balances stay within the signed 64-bit range.
  • A mutation should not scan prior entries. Returning n balances costs O(n)O(n); returning e entries with p total postings costs O(e+p)O(e+p).

Notes

The judge uses one thread per testcase and preserves call order. Concurrent calls on one instance are outside scope, and different instances share no state. Multiple groups, membership changes, currencies, exchange rates, taxes, percentage splits, timestamps, descriptions, persistence, editing, deletion, debt minimization, and automatic idempotency beyond duplicate entry rejection are outside scope.

PRIVATE WORKSPACE

Checking your session…

The statement is public. The editor, editorial, submissions, and saved work are private.