Splits income across budget lines so every dollar is assigned and every budget is funded — by balancing a matrix, not by filling one bucket at a time.
The obvious approach is to sort budgets by priority and pour income in until it runs out. It produces a plan that is right in total and wrong in detail.
It drains one paycheck completely before touching the next, so it cannot express "this deposit covers most of the mortgage and a little of groceries" — which is what actually happens. And because each budget is solved in isolation, any constraint you add later has nowhere to live: an account that must receive a fixed amount, income that shouldn't fund a particular category, a source that lands mid-month.
This solves the whole grid at once using iterative proportional fitting — the technique used to balance transportation matrices and contingency tables.
Build a source-by-budget matrix, then alternately scale rows to hit each source's total and columns to hit each budget's target. Repeat until both hold. Entries stay non-negative throughout, and the two constraints are satisfied together rather than in sequence.
mortgage groceries fun = source
paycheck 1,732 892 376 3,000
rental 768 608 624 2,000
─────────────────────────────────────────────────
= budget 2,500 1,500 1,000 5,000
Rows sum to what each source provides. Columns sum to what each budget needs.
Sources carry a stability rank, budgets carry a priority rank. These steer
dependable income toward the budgets you least want to miss — the mortgage gets
salary, the hobby fund gets freelance work.
They only seed the starting matrix. They never decide whether the plan balances, so a strong preference can't produce an unbalanced result.
They usually don't, and both constraints can only hold when they do. Rather than refusing to answer, the difference is absorbed in a slack row or column:
- Income exceeds budgets → every budget is funded in full, and the remainder
is reported as
surplus - Budgets exceed income → every dollar is spent, budgets are underfunded
proportionally, and the gap is reported as
shortfall
Neither distorts the real allocations, and neither is silently swallowed.
import { allocate } from "./src/domain/allocate";
const result = allocate(
[
{ id: "paycheck", label: "Paycheck", amount: 3000, stability: 1 },
{ id: "rental", label: "Rental", amount: 2000, stability: 3 },
],
[
{ id: "mortgage", label: "Mortgage", amount: 2500, priority: 1 },
{ id: "groceries", label: "Groceries", amount: 1500, priority: 2 },
{ id: "fun", label: "Fun", amount: 1000, priority: 5 },
],
);
result.transfers; // [{ sourceId, budgetId, amount }, ...] largest first
result.surplus; // 0
result.converged; // trueDownloads a workbook that still calculates. Not a dump of the numbers the app worked out — the balancing itself runs as Excel formulas, so changing an income figure on the Inputs sheet rebalances the whole matrix with no app and no internet.
| Sheet | |
|---|---|
| Read me | What the model does |
| Inputs | The only sheet you edit |
| Calc | The seed matrix and 24 balancing passes |
| Transfers | What to move from each source to each line |
| Check | Allocated minus target for every row and column |
Twenty-four passes because measured convergence on realistic plans is 11–18 rounds to a residual under 1e-6. The Check sheet reports the largest remaining difference rather than asking you to take that on trust.
ExcelJS is loaded on demand — it is roughly 900KB, and bundling it eagerly made every visitor pay for a spreadsheet most sessions never ask for.
35 tests, asserting the properties rather than a snapshot of current output: rows sum to their source, columns sum to their target, nothing is ever negative, surplus and shortfall are reported rather than hidden, a strong affinity still balances, output is deterministic, and inputs are never mutated. The export is covered too: that transfers are formulas rather than baked values, that passes alternate and end on a column pass, that the check avoids array formulas, and that columns past Z still resolve.
npm install
npm test
npm run typecheckAssertions are written against the tolerance the allocator promises (1e-6 on row and column totals) rather than a decimal-place count — demanding more precision than the contract is how the suite first went red against correct code.
The engine is done and tested. A browser UI is next: budget figures are exactly the kind of data that should never leave the device, so it will run entirely client-side with no backend, the same way the Rental Property Analyzer does.