diff --git a/src/money/cents.ts b/src/money/cents.ts index f6321d8..0c9b693 100644 --- a/src/money/cents.ts +++ b/src/money/cents.ts @@ -29,22 +29,20 @@ export function parseCents(input: string): number { /** * Split an integer amount of cents evenly across member ids. * - * SEEDED DEFECT (issue #2): this floors every share and drops the - * remainder instead of handing the leftover cents to the first N members, - * so the shares can sum to less than totalCents. `bun test` does not catch - * this because the baseline tests only split amounts that divide evenly. - * The correct remainder rule is documented in + * The first `remainder` members (in the order given) get one extra cent, so + * the shares always sum to exactly totalCents. See the remainder rule in * .claude/skills/handling-money/references/rules.md. */ export function splitCents(totalCents: number, memberIds: string[]): Record { if (memberIds.length === 0) { throw new Error("cannot split among zero members"); } - const share = Math.floor(totalCents / memberIds.length); + const base = Math.floor(totalCents / memberIds.length); + const remainder = totalCents - base * memberIds.length; const shares: Record = {}; - for (const id of memberIds) { - shares[id] = share; - } + memberIds.forEach((id, i) => { + shares[id] = i < remainder ? base + 1 : base; + }); return shares; } diff --git a/tests/money.test.ts b/tests/money.test.ts index ed1ee3f..d8a3ad1 100644 --- a/tests/money.test.ts +++ b/tests/money.test.ts @@ -36,4 +36,16 @@ describe("splitCents", () => { const shares = splitCents(2000, ["a", "b"]); expect(sumCents(Object.values(shares))).toBe(2000); }); + + test("gives the leftover cent to the first member (issue #178)", () => { + const shares = splitCents(1000, ["a", "b", "c"]); + expect(shares).toEqual({ a: 334, b: 333, c: 333 }); + expect(sumCents(Object.values(shares))).toBe(1000); + }); + + test("gives leftover cents to the first N members in order", () => { + const shares = splitCents(1001, ["a", "b", "c"]); + expect(shares).toEqual({ a: 334, b: 334, c: 333 }); + expect(sumCents(Object.values(shares))).toBe(1001); + }); }); diff --git a/tests/routes.test.ts b/tests/routes.test.ts index 25cac54..2ae4dcf 100644 --- a/tests/routes.test.ts +++ b/tests/routes.test.ts @@ -116,9 +116,6 @@ describe("POST and DELETE /api/groups/:id/expenses", () => { describe("GET /api/groups/:id/balances", () => { test("transfers settle every balance to zero net", async () => { - // Flat 4B's seeded amounts all divide evenly by its 3 members, so this - // fixture is unaffected by the splitCents remainder bug (issue #2) and - // exercises settle-up on its own. const app = setupApp(); const res = await app.request("/api/groups/flat-4b/balances", { headers: auth("flat-priya") }); const body = (await res.json()) as { balances: { netCents: number }[]; transfers: unknown[] };