diff --git a/src/money/cents.ts b/src/money/cents.ts index f6321d8..96067d5 100644 --- a/src/money/cents.ts +++ b/src/money/cents.ts @@ -29,22 +29,21 @@ 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 - * .claude/skills/handling-money/references/rules.md. + * Remainder rule (.claude/skills/handling-money/references/rules.md): every + * member gets floor(totalCents / n), and the first `remainder` members, in + * the order passed in, get one extra cent, so the shares always sum to + * exactly totalCents. */ 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..5ca6b7f 100644 --- a/tests/money.test.ts +++ b/tests/money.test.ts @@ -36,4 +36,28 @@ describe("splitCents", () => { const shares = splitCents(2000, ["a", "b"]); expect(sumCents(Object.values(shares))).toBe(2000); }); + + test("gives the leftover cent to the first member when splitting 1000 three ways", () => { + const shares = splitCents(1000, ["a", "b", "c"]); + expect(shares).toEqual({ a: 334, b: 333, c: 333 }); + expect(sumCents(Object.values(shares))).toBe(1000); + }); + + test("assigns leftover cents so the shares sum to the total", () => { + const shares = splitCents(101, ["a", "b", "c", "d", "e"]); + expect(shares).toEqual({ a: 21, b: 20, c: 20, d: 20, e: 20 }); + expect(sumCents(Object.values(shares))).toBe(101); + }); + + test("gives leftover cents to the first members in member order", () => { + const shares = splitCents(1002, ["c", "a", "d", "b"]); + expect(shares).toEqual({ c: 251, a: 251, d: 250, b: 250 }); + expect(sumCents(Object.values(shares))).toBe(1002); + }); + + test("splits a total smaller than the member count one cent at a time", () => { + const shares = splitCents(2, ["a", "b", "c", "d", "e"]); + expect(shares).toEqual({ a: 1, b: 1, c: 0, d: 0, e: 0 }); + expect(sumCents(Object.values(shares))).toBe(2); + }); });