Skip to content

Instantly share code, notes, and snippets.

@geetduggal
Created February 19, 2024 07:25
Show Gist options
  • Save geetduggal/20d8eb1b81dec65fd7be6d2999a3cbdc to your computer and use it in GitHub Desktop.
Save geetduggal/20d8eb1b81dec65fd7be6d2999a3cbdc to your computer and use it in GitHub Desktop.
Financial Dashboard in Obsidian: Ledger + Dataview
// 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