Skip to content

Instantly share code, notes, and snippets.

@jedrichards
Last active August 29, 2015 14:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jedrichards/22fa15dd89fe34c3eec1 to your computer and use it in GitHub Desktop.
Save jedrichards/22fa15dd89fe34c3eec1 to your computer and use it in GitHub Desktop.
Group expenses
// * 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