diff --git a/CHANGES.md b/CHANGES.md index 9c38282..6455edb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased +- Add splitBill(items, people) to split the taxed total into per-person shares. ## 1.2.0 - total() rounds through roundCents(). diff --git a/src/lib/cart.js b/src/lib/cart.js index 385fd69..bb38bb5 100644 --- a/src/lib/cart.js +++ b/src/lib/cart.js @@ -8,3 +8,16 @@ export function subtotal(items) { export function total(items) { return roundCents(subtotal(items) * (1 + TAX_RATE)); } + +// Splits the taxed total between people (a count or an array), in whole cents. +// Leftover cents go one each to the first people, so shares always sum to total(). +export function splitBill(items, people) { + const n = Array.isArray(people) ? people.length : people; + if (!Number.isInteger(n) || n < 1) { + throw new RangeError("people must be a positive integer or a non-empty array"); + } + const cents = Math.trunc(total(items) * 100 + 0.5); + const base = Math.trunc(cents / n); + const extra = cents - base * n; + return Array.from({ length: n }, (_, i) => roundCents((base + (i < extra ? 1 : 0)) / 100)); +} diff --git a/tests/lib/cart.test.js b/tests/lib/cart.test.js index 7a9eab3..a4eed3b 100644 --- a/tests/lib/cart.test.js +++ b/tests/lib/cart.test.js @@ -1,6 +1,6 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { subtotal, total } from '../../src/lib/cart.js'; +import { subtotal, total, splitBill } from '../../src/lib/cart.js'; const items = [{ price: 10, qty: 2 }, { price: 5, qty: 1 }]; @@ -11,3 +11,39 @@ test('subtotal adds price times quantity', () => { test('total applies tax and rounds to cents', () => { assert.equal(total(items), 27); }); + +const sum = (xs) => Math.round(xs.reduce((a, b) => a + b, 0) * 100) / 100; + +test('splitBill splits the taxed total evenly', () => { + assert.deepEqual(splitBill(items, 3), [9, 9, 9]); +}); + +test('splitBill accepts an array of people', () => { + assert.deepEqual(splitBill(items, ['a', 'b']), [13.5, 13.5]); +}); + +test('splitBill gives one person the whole total', () => { + assert.deepEqual(splitBill(items, 1), [27]); +}); + +test('splitBill distributes leftover cents so shares sum to the total', () => { + const one = [{ price: 10, qty: 1 }]; // total 10.80 + const shares = splitBill(one, 7); + assert.equal(shares.length, 7); + assert.equal(sum(shares), total(one)); + assert.ok(Math.max(...shares) - Math.min(...shares) <= 0.01 + 1e-9); +}); + +test('splitBill gives extra cents to the first people', () => { + assert.deepEqual(splitBill([{ price: 0.1, qty: 1 }], 4), [0.03, 0.03, 0.03, 0.02]); // total 0.11 +}); + +test('splitBill of an empty cart is all zeros', () => { + assert.deepEqual(splitBill([], 3), [0, 0, 0]); +}); + +test('splitBill rejects invalid people', () => { + for (const bad of [0, -1, 1.5, NaN, [], undefined, '3']) { + assert.throws(() => splitBill(items, bad), RangeError); + } +});