-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
coin2html: refactoring the source files
- Loading branch information
Showing
12 changed files
with
142,857 additions
and
2,309 deletions.
There are no files selected for viewing
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import { Commodity } from "../src/models"; | ||
import { Commodity } from "../src/commodity"; | ||
|
||
test("create commodity", () => | ||
expect(new Commodity("CAD", "Canadian Dollar", 2, "")).toBeTruthy()); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
import { Amount, Commodities, Commodity } from "./commodity"; | ||
import { State } from "./ui"; | ||
import { | ||
AccountPostingGroups, | ||
dateToString, | ||
groupBy, | ||
trimToDateRange, | ||
} from "./utils"; | ||
|
||
export class Account { | ||
children: Account[] = []; | ||
postings: Posting[] = []; | ||
constructor( | ||
readonly name: string, | ||
readonly fullName: string, | ||
readonly commodity: Commodity, | ||
readonly parent: Account, | ||
readonly location: string, | ||
readonly closed?: Date | ||
) { | ||
if (parent) { | ||
parent.children.push(this); | ||
} | ||
} | ||
allChildren(): Account[] { | ||
return this.children.reduce( | ||
(total: Account[], acc: Account) => | ||
State.ShowClosedAccounts || !acc.closed | ||
? total.concat([acc, ...acc.allChildren()]) | ||
: total, | ||
[] | ||
); | ||
} | ||
toString(): string { | ||
return this.fullName; | ||
} | ||
// child name with this account's name prefix stripped | ||
relativeName(child: Account): string { | ||
return child.fullName.slice(this.fullName.length); | ||
} | ||
withAllChildPostings(from: Date, to: Date): Posting[] { | ||
const postings = trimToDateRange(this.postings, from, to).concat( | ||
this.children.map((c) => c.withAllChildPostings(from, to)).flat() | ||
); | ||
postings.sort( | ||
(a, b) => a.transaction.posted.getTime() - b.transaction.posted.getTime() | ||
); | ||
return postings; | ||
} | ||
withAllChildPostingGroups( | ||
from: Date, | ||
to: Date, | ||
groupKey: d3.TimeInterval | ||
): AccountPostingGroups[] { | ||
let accounts = this.allChildren(); | ||
accounts.unshift(this); | ||
// drop accounts with no postings | ||
accounts = accounts.filter((a) => a.postings.length > 0); | ||
return accounts.map((acc) => ({ | ||
account: acc, | ||
groups: groupBy( | ||
trimToDateRange(acc.postings, from, to), | ||
groupKey, | ||
(p) => p.transaction.posted, | ||
acc.commodity | ||
), | ||
})); | ||
} | ||
withAllParents(): Account[] { | ||
return this.parent ? this.parent.withAllParents().concat(this) : [this]; | ||
} | ||
getRootAccount(): Account { | ||
return this.parent ? this.parent.getRootAccount() : this; | ||
} | ||
} | ||
|
||
export interface Tags { | ||
[key: string]: string; | ||
} | ||
|
||
export class Posting { | ||
index?: number; // used to cache index in the register view for sorting | ||
constructor( | ||
readonly transaction: Transaction, | ||
readonly account: Account, | ||
readonly quantity: Amount, | ||
readonly balance: Amount, | ||
readonly balance_asserted?: boolean, | ||
readonly notes?: string[], | ||
readonly tags?: Tags | ||
) { | ||
transaction.postings.push(this); | ||
account.postings.push(this); | ||
} | ||
toString(): string { | ||
return ( | ||
this.account.fullName + | ||
" " + | ||
this.quantity.toString() + | ||
(this.balance_asserted ? " = " + this.balance.toString() : "") | ||
); | ||
} | ||
} | ||
|
||
export class Transaction { | ||
postings: Posting[] = []; | ||
constructor( | ||
readonly posted: Date, | ||
readonly description: string, | ||
readonly location: string, | ||
readonly notes?: string[], | ||
readonly code?: string | ||
) {} | ||
toString(): string { | ||
return dateToString(this.posted) + " " + this.description; | ||
} | ||
// return the other posting in this transaction | ||
// less clear in multi-posting transactions | ||
// return first posting that isn't notThis and has the opposite sign | ||
other(notThis: Posting): Posting { | ||
const notThisSign = notThis.quantity.sign; | ||
for (const p of this.postings) { | ||
if (p != notThis && (p.quantity.sign != notThisSign || notThisSign == 0)) | ||
return p; | ||
} | ||
throw new Error(`No other posting? ${notThis}`); | ||
} | ||
} | ||
|
||
type importedAccounts = Record< | ||
string, | ||
{ | ||
name: string; | ||
fullName: string; | ||
commodity: string; | ||
parent: string; | ||
closed?: string; | ||
location: string; | ||
} | ||
>; | ||
type importedTransactions = { | ||
posted: string; | ||
description: string; | ||
location: string; | ||
postings: { | ||
account: string; | ||
balance: string; | ||
balance_asserted: boolean; | ||
quantity: string; | ||
notes?: string[]; | ||
tags?: Tags; | ||
}[]; | ||
notes?: string[]; | ||
code?: string; | ||
tags?: Tags; | ||
}[]; | ||
|
||
// min and max transaction date from the dataset | ||
export let MinDate = new Date(); | ||
export let MaxDate = new Date(0); | ||
|
||
export const Accounts: Record<string, Account> = {}; | ||
export const Roots: Account[] = []; | ||
export function loadAccounts() { | ||
const importedAccounts = JSON.parse( | ||
document.getElementById("importedAccounts")!.innerText | ||
) as importedAccounts; | ||
for (const impAccount of Object.values(importedAccounts)) { | ||
if (impAccount.name == "Root") continue; | ||
const parent = Accounts[impAccount.parent]; | ||
const account = new Account( | ||
impAccount.name, | ||
impAccount.fullName, | ||
Commodities[impAccount.commodity], | ||
parent, | ||
impAccount.location, | ||
impAccount.closed ? new Date(impAccount.closed) : undefined | ||
); | ||
Accounts[account.fullName] = account; | ||
if (!parent) { | ||
Roots.push(account); | ||
} | ||
} | ||
|
||
const importedTransactions = JSON.parse( | ||
document.getElementById("importedTransactions")!.innerText | ||
) as importedTransactions; | ||
for (const impTransaction of Object.values(importedTransactions)) { | ||
const posted = new Date(impTransaction.posted); | ||
if (posted < MinDate) MinDate = posted; | ||
if (MaxDate < posted) MaxDate = posted; | ||
const transaction = new Transaction( | ||
posted, | ||
impTransaction.description, | ||
impTransaction.location, | ||
impTransaction.notes, | ||
impTransaction.code | ||
); | ||
for (const impPosting of impTransaction.postings) { | ||
const account = Accounts[impPosting.account]; | ||
if (!account) { | ||
throw new Error("Unknown account: " + impPosting.account); | ||
} | ||
const quantity = Amount.parse(impPosting.quantity); | ||
const balance = Amount.parse(impPosting.balance); | ||
const posting = new Posting( | ||
transaction, | ||
account, | ||
quantity, | ||
balance, | ||
impPosting.balance_asserted, | ||
impPosting.notes, | ||
impPosting.tags | ||
); | ||
} | ||
} | ||
MinDate = new Date(MinDate.getFullYear(), 0, 1); | ||
MaxDate = new Date(MaxDate.getFullYear(), 11, 31); | ||
} |
Oops, something went wrong.