-
-
Save jedrichards/22fa15dd89fe34c3eec1 to your computer and use it in GitHub Desktop.
Group expenses
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// * Create expense categories here. | |
var cats = { | |
ALL: "All", | |
JC_AND_JS_FUEL: "Jed & Clare's fuel", | |
SK_AND_L_FUEL: "Seb's fuel", | |
COTTAGE_BREAKFAST: "Cottage breakfast", | |
COTTAGE_FOOD: "Cottage food supplies" | |
} | |
// * Amount tolerance. Any amounts owing below this threshold are ignored. | |
var amountTolerance = 5; | |
// Arrays for holding data structures | |
var expenses = []; | |
var members = []; | |
var paymentPairs = []; | |
// PaymentPair class | |
var PaymentPair = function() { | |
this.creditor = null; | |
this.debtor = null; | |
this.amount = 0; | |
} | |
// Member class | |
var Member = function (name) { | |
this.name = name; | |
this.exemptions = []; | |
this.expenditure = 0; | |
this.liability = 0; | |
this.owes = 0; | |
} | |
Member.prototype.addExemption = function (cat) { | |
this.exemptions.push({cat:cat}); | |
console.log(this.name,"is exempt from",cat); | |
} | |
Member.prototype.addExpenditure = function (value) { | |
this.expenditure += value; | |
} | |
Member.prototype.addLiability = function (value) { | |
this.liability += value; | |
this.owes = this.liability-this.expenditure; | |
} | |
Member.prototype.isExempt = function (cat) { | |
var isExempt = false; | |
this.exemptions.forEach(function (exemption) { | |
if ( exemption.cat === cat ) { | |
isExempt = true; | |
} | |
}); | |
return isExempt; | |
} | |
Member.prototype.toString = function () { | |
return this.name; | |
} | |
// Expense class | |
var Expense = function (description,cat,amount) { | |
this.description = description; | |
this.cat = cat; | |
this.amount = amount; | |
this.owner = null; | |
this.participants = []; | |
this.costPerParticipant = 0; | |
} | |
Expense.prototype.setOwner = function (owner) { | |
this.owner = owner; | |
} | |
Expense.prototype.addParticipant = function (participant) { | |
this.participants.push(participant); | |
this.costPerParticipant = this.amount/this.participants.length; | |
} | |
// Util functions | |
function addMember(name) { | |
console.log(name); | |
var member = new Member(name); | |
members.push(member); | |
return member; | |
} | |
function addExpense (description,cat,amount) { | |
console.log(description,"£"+amount); | |
var expense = new Expense(description,cat,amount); | |
expenses.push(expense); | |
return expense; | |
} | |
function addPaymentPair (creditor,debtor,amount) { | |
var pair = new PaymentPair(); | |
pair.creditor = creditor; | |
pair.debtor = debtor; | |
pair.amount = amount; | |
paymentPairs.push(pair); | |
} | |
function getBiggestCreditor () { | |
var biggestCreditor; | |
members.forEach(function (member) { | |
if ( !biggestCreditor ) { | |
biggestCreditor = member; | |
} | |
if ( biggestCreditor.owes > member.owes ) { | |
biggestCreditor = member; | |
} | |
}); | |
if ( biggestCreditor.owes+amountTolerance > 0 ) { | |
return null; | |
} else { | |
return biggestCreditor; | |
} | |
} | |
function getBiggestDebtor () { | |
var biggestDebtor; | |
members.forEach(function (member) { | |
if ( !biggestDebtor ) { | |
biggestDebtor = member; | |
} | |
if ( biggestDebtor.owes < member.owes ) { | |
biggestDebtor = member; | |
} | |
}); | |
// if ( biggestDebtor.owes-amountTolerance < 0 ) { | |
// return null; | |
// } else { | |
// return biggestDebtor; | |
// } | |
return biggestDebtor.owes === 0 ? null : biggestDebtor; | |
} | |
// * Create members | |
console.log("\nCreating members"); | |
console.log("----------------"); | |
var seb = addMember("Seb"); | |
var jedClare = addMember("Jed & Clare"); | |
var luke = addMember("Luke"); | |
var jeromeSarah = addMember("Jerome & Sarah"); | |
// * Set exemptions. Members can be excluded from expenses by category. | |
console.log("\nSetting exemptions"); | |
console.log("------------------"); | |
jedClare.addExemption(cats.SK_AND_L_FUEL); | |
jeromeSarah.addExemption(cats.SK_AND_L_FUEL); | |
seb.addExemption(cats.JC_AND_JS_FUEL); | |
luke.addExemption(cats.JC_AND_JS_FUEL); | |
jeromeSarah.addExemption(cats.COTTAGE_BREAKFAST); | |
seb.addExemption(cats.COTTAGE_FOOD); | |
// * Create expenses. Expenses have a name, category, amount and owner. | |
// Members are assumed to be equally liable for all expeses unless they are | |
// specifically excluded from an expenses category. | |
console.log("\nCreating expenses"); | |
console.log("-----------------"); | |
var expense; | |
expense = addExpense("Cottage Breakfast Meat",cats.COTTAGE_BREAKFAST,20); | |
expense.setOwner(jedClare); | |
expense = addExpense("Petrol",cats.JC_AND_JS_FUEL,40); | |
expense.setOwner(jedClare); | |
expense = addExpense("Cottage Breakfast Extras",cats.COTTAGE_BREAKFAST,6); | |
expense.setOwner(luke); | |
expense = addExpense("Whisky",cats.ALL,20); | |
expense.setOwner(luke); | |
expense = addExpense("Petrol",cats.SK_AND_L_FUEL,50); | |
expense.setOwner(seb); | |
expense = addExpense("Cottage food supplies",cats.COTTAGE_FOOD,38); | |
expense.setOwner(seb); | |
// * Nothing more to edit blow. | |
// Calculate expense owner expenditures | |
expenses.forEach(function (expense) { | |
expense.owner.addExpenditure(expense.amount); | |
}); | |
// Add participants to expenses | |
console.log("\nExpense detail"); | |
console.log("--------------"); | |
expenses.forEach(function (expense) { | |
console.log(expense.description); | |
console.log(" Owner",expense.owner.name); | |
console.log(" Category",expense.cat); | |
console.log(" Amount","£"+expense.amount); | |
members.forEach(function (member) { | |
if ( !member.isExempt(expense.cat) ) { | |
expense.addParticipant(member); | |
} | |
}); | |
console.log(" Participants",expense.participants.length,"("+expense.participants.join(",")+")"); | |
console.log(" Cost per participant","£"+expense.costPerParticipant.toFixed(2)); | |
}); | |
// Calculate member liabilities | |
console.log("\nMember detail"); | |
console.log("-------------"); | |
expenses.forEach(function (expense) { | |
expense.participants.forEach(function (participant) { | |
participant.addLiability(expense.costPerParticipant); | |
}); | |
}); | |
// Output member detail | |
var totalExpenditure = 0; | |
var totalLiability = 0; | |
members.forEach(function (member) { | |
console.log(member.name); | |
console.log(" Expenditure","£"+member.expenditure); | |
console.log(" Liability","£"+member.liability.toFixed(2)); | |
totalExpenditure += member.expenditure; | |
totalLiability += member.liability; | |
console.log(" Owes","£"+member.owes.toFixed(2)); | |
}); | |
// Sanity check stats | |
console.log("\nStats"); | |
console.log("-----"); | |
console.log("Num expenses",expenses.length); | |
console.log("Avg expense value","£"+(totalExpenditure/expenses.length).toFixed(2)); | |
console.log("Total spent","£"+totalExpenditure); | |
//console.log("Total liability","£"+totalLiability.toFixed(2)); | |
// Calculate payments for resolution | |
var biggestCreditor = getBiggestCreditor(); | |
while ( biggestCreditor !== null ) { | |
var biggestDebtor = getBiggestDebtor(); | |
if ( biggestDebtor === null ) { | |
break; | |
} | |
if ( biggestDebtor.owes > Math.abs(biggestCreditor.owes) ) { | |
addPaymentPair(biggestCreditor,biggestDebtor,Math.abs(biggestCreditor.owes)); | |
biggestDebtor.owes += biggestCreditor.owes; | |
biggestCreditor.owes = 0; | |
} else { | |
addPaymentPair(biggestCreditor,biggestDebtor,biggestDebtor.owes); | |
biggestCreditor.owes += biggestDebtor.owes; | |
biggestDebtor.owes = 0; | |
} | |
biggestCreditor = getBiggestCreditor(); | |
} | |
// Sort payments by creditor name | |
paymentPairs.sort(function (a,b) { | |
var nameA = a.creditor.name.toLowerCase(); | |
var nameB = b.creditor.name.toLowerCase(); | |
if ( nameA < nameB ) { | |
return -1; | |
} else if ( nameA > nameB ) { | |
return 1; | |
} else { | |
return 0; | |
} | |
}); | |
// Output payment instructions | |
console.log("\nPayments"); | |
console.log("--------"); | |
paymentPairs.forEach(function (pair) { | |
console.log(pair.debtor.name,"pays",pair.creditor.name,"£"+Math.round(pair.amount)); | |
}); | |
console.log("\nRemainders"); | |
console.log("----------"); | |
members.forEach(function (member) { | |
console.log(member.name,"£"+member.owes); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment