Created
February 19, 2024 07:25
-
-
Save geetduggal/20d8eb1b81dec65fd7be6d2999a3cbdc to your computer and use it in GitHub Desktop.
Financial Dashboard in Obsidian: Ledger + Dataview
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
// Code for https://medium.com/@geetduggal/the-power-of-obsidian-for-personal-finance-accounting-f00bbcb1b66f | |
// Step 1: SUPER shameful poor-man parsing of a ledger journal file | |
const ledgerFile = await dv.io.load("🔲 Spaces/💰 Finances/transactions.ledger.md") | |
let allTransactions = ledgerFile.split(/\n\n/). | |
filter((transactionText) => { | |
return !(transactionText.startsWith(";") || transactionText.startsWith("alias") || transactionText.match(/^\s*$/)) | |
}). | |
map((transactionText) => { | |
let transactionLines = transactionText.split("\n") | |
let transaction = {} | |
transaction.date = transactionLines[0].slice(0,10) | |
transaction.description = transactionLines[0].slice(10) | |
transaction.entries = transactionLines.slice(1).map((e)=>{ | |
let eSplit = e.trim().split("$") | |
return {account: eSplit[0].trim(), amount: parseFloat(eSplit[1]), date: transaction.date, description:transaction.description} | |
}) | |
let transactionSum = transaction.entries.map((e)=>e.amount).filter((x)=>!isNaN(x)).reduce((a,b)=>a+b) | |
transaction.entries = transaction.entries.map((e)=>{ | |
if (isNaN(e.amount)) { e.amount = -1*transactionSum} | |
return e | |
}) | |
return transaction | |
}) | |
// Step 2 | |
let currentDate = new Date().toISOString().slice(0,10) | |
let accountsSummary = allTransactions.map((t)=>t.entries).flat().reduce((acc, t)=>{ | |
let subAccounts = t.account.split(":") | |
let prefixAccount = "" | |
subAccounts.forEach((a)=>{ | |
prefixAccount += a | |
if (!acc[prefixAccount]) { | |
acc[prefixAccount] = {balance: 0, futureBalance:0, transactions: []} | |
} | |
if (t.date <= currentDate) { acc[prefixAccount].balance += t.amount } | |
acc[prefixAccount].futureBalance += t.amount | |
acc[prefixAccount].transactions.push(t) | |
prefixAccount += ":" | |
}) | |
return acc | |
}, {}) | |
// Step 3 | |
function bold(x) {return `<b>${x}</b>`} | |
function dollars(x) {return "$"+Math.round(x)} | |
function color(c,x) { return "<font style='color:" + c + "'>" + x + "</font>" } | |
dv.header(1, "Balance Summary") | |
dv.table( | |
["Account", "Balance", "Future Balance"], | |
Object.keys(accountsSummary).sort().map((a)=>{ | |
let balance = accountsSummary[a].balance | |
let futureBalance = accountsSummary[a].futureBalance | |
balance = dollars(balance) | |
futureBalance = dollars(futureBalance) | |
if (accountsSummary[a].futureBalance < 0) { | |
futureBalance = color("red", futureBalance) | |
} | |
if(a.split(":").length == 1) { | |
a = bold(a) | |
balance = bold(balance) | |
futureBalance = bold(futureBalance) | |
} | |
return [a, balance, futureBalance] | |
}) | |
) | |
function cdate( a, b ) { | |
if ( a.date < b.date ){ | |
return -1; | |
} | |
if ( a.date > b.date ){ | |
return 1; | |
} | |
return 0; | |
} | |
let fileDate = dv.current().file.name.slice(0,10) | |
// Check to see if the first characters match ISO 8601 validation | |
if (!fileDate.match(/^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/)) { fileDate = "!"} | |
// Step 4 | |
dv.header(2, "Unknown") | |
let total = 0 | |
dv.table( | |
["Date", "Account", "Description", "Amount"], | |
allTransactions.map((x)=>x.entries).flat().sort(cdate).filter((x)=>x.date >= fileDate && (x.account.includes("Unknown") || x.description.includes("Unknown")) && x.account.slice(0,1) === "E").map((x)=> { | |
let amount = x.amount | |
if (x.amount > 0) { total += x.amount } | |
if (Math.abs(x.amount) > 100) { | |
amount = bold(amount) | |
x.description = bold(x.description) | |
} | |
return [x.date, x.account, x.description, amount] | |
}) | |
) | |
dv.paragraph(bold('<div style="text-align: right">Total Unknown: '+Math.round(total)+'</div>')) | |
Object.keys(accountsSummary).sort().forEach((a)=>{ | |
let total = 0 | |
if(a.split(":").length != 1) { | |
dv.header(2, a) | |
dv.table( | |
["Date", "Account", "Description", "Amount"], | |
accountsSummary[a].transactions.sort(cdate).filter((x)=>x.date >= fileDate).map((x)=> { | |
let amount = x.amount | |
if (x.amount > 0) { total += x.amount } | |
if (Math.abs(x.amount) > 100) { | |
amount = bold(amount) | |
x.description = bold(x.description) | |
} | |
return [x.date, x.account, x.description, amount] | |
}) | |
) | |
if (a.slice(0,1) === "E") { | |
dv.paragraph(bold('<div style="text-align: right">Total Expenses: $'+Math.round(total)+'</div>')) | |
} | |
} | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment