From 94c5b9169b76e7b18955f2de1d01dd039085c658 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Tue, 10 Oct 2017 20:44:29 -0700 Subject: [PATCH 01/28] removed computer-specific debug arguments --- src/fsharp/Ledger/Ledger.fsproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/fsharp/Ledger/Ledger.fsproj b/src/fsharp/Ledger/Ledger.fsproj index 70660e9..5e1d4e7 100644 --- a/src/fsharp/Ledger/Ledger.fsproj +++ b/src/fsharp/Ledger/Ledger.fsproj @@ -25,8 +25,10 @@ AnyCPU bin\Debug\Ledger.XML true - Little-Red-Hen.ledger chart-of-accounts - C:\Users\mafm\Desktop\working-directories\family-taxes + + + + pdbonly From f229f47b215aaff3ea6e28ff2a435eccf02b7dd3 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Tue, 10 Oct 2017 20:47:43 -0700 Subject: [PATCH 02/28] added additional samples showing partial support for multiple entities --- examples/entity_sample.transactions | 18 ++++++++++++++++++ examples/unbalanced_entity_sample.transactions | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 examples/entity_sample.transactions create mode 100644 examples/unbalanced_entity_sample.transactions diff --git a/examples/entity_sample.transactions b/examples/entity_sample.transactions new file mode 100644 index 0000000..51bc27e --- /dev/null +++ b/examples/entity_sample.transactions @@ -0,0 +1,18 @@ +2013-01-01 I began the year with $1000 in my cheque account. + John/Assets:Bankwest:Cheque $1,000 + John/Equity:OpeningBalances $1,000 + +2013-01-05 I bought some groceries and paid using the cheque account. + Jacob/Expenses:Food:Groceries $98.53 + Jacob/Assets:Bankwest:Cheque -$98.53 + +2013-01-10 I bought some petrol, and paid using a credit card. + Expenses:Motor:Fuel $58.01 + Liabilities:Bankwest:Visa $58.01 + +2013-01-15 I paid my electricity bill. + Expenses:Electricity $280.42 + Assets:Bankwest:Cheque -$280.42 + +# I checked my bank statement on the 1st of Feb, and this is what it said. +VERIFY-BALANCE 2013-02-01 Assets:Bankwest:Cheque 621.05 diff --git a/examples/unbalanced_entity_sample.transactions b/examples/unbalanced_entity_sample.transactions new file mode 100644 index 0000000..c54122f --- /dev/null +++ b/examples/unbalanced_entity_sample.transactions @@ -0,0 +1,18 @@ +2013-01-01 I began the year with $1000 in my cheque account. + John/Assets:Bankwest:Cheque $1,000 + John/Equity:OpeningBalances $1,000 + +2013-01-05 I bought some groceries and paid using the cheque account. + Jacob/Expenses:Food:Groceries $98.53 + John/Assets:Bankwest:Cheque -$98.53 + +2013-01-10 I bought some petrol, and paid using a credit card. + Jacob/Expenses:Motor:Fuel $58.01 + Liabilities:Bankwest:Visa $58.01 + +2013-01-15 I paid my electricity bill. + Expenses:Electricity $280.42 + Assets:Bankwest:Cheque -$280.42 + +# I checked my bank statement on the 1st of Feb, and this is what it said. +VERIFY-BALANCE 2013-02-01 Assets:Bankwest:Cheque 621.05 From 792cbd6584b2c2f0236e0341bec1cb0e50c7ed6e Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Tue, 10 Oct 2017 20:48:01 -0700 Subject: [PATCH 03/28] Modified to work with current entity model --- src/fsharp/Ledger/TextOutput.fs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/fsharp/Ledger/TextOutput.fs b/src/fsharp/Ledger/TextOutput.fs index 5d9d455..5127c2f 100644 --- a/src/fsharp/Ledger/TextOutput.fs +++ b/src/fsharp/Ledger/TextOutput.fs @@ -10,14 +10,16 @@ open InternalTypes type Text = static member fmt (x:InternalNameAccount) = - match x with - | last::[] -> - match last.Input with - (Input name) -> (sprintf "%s" name) - | first::rest -> - match first.Input with - (Input name) -> (sprintf "%s:%s" name (Text.fmt rest)) - | [] -> raise EmptyAccountNameComponentsException + let entity = x.[0].Input.Entity.AsString //XXX: There has to be a better way, perhaps InternalNameAccount will have Entity as a top level field? + (sprintf "%s/" entity) + (match x with + | last::[] -> + match last.Input with + (InputName (entity, name)) -> (sprintf "%s" name) + | first::rest -> + match first.Input with + (InputName (entity, name)) -> (sprintf "%s:%s" name (Text.fmt rest)) + | [] -> raise EmptyAccountNameComponentsException) + static member fmt (x: Amount) = match x with | AUD 0 -> "-" From 10293394a8421136f689e7e5fdd29c71a8d1fcfc Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Tue, 10 Oct 2017 20:50:19 -0700 Subject: [PATCH 04/28] added ability to optionally parse entities in the format "entity/account:subaccount" format --- src/fsharp/Ledger/Parse.fs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/fsharp/Ledger/Parse.fs b/src/fsharp/Ledger/Parse.fs index 8dfde87..d03f909 100644 --- a/src/fsharp/Ledger/Parse.fs +++ b/src/fsharp/Ledger/Parse.fs @@ -27,10 +27,23 @@ let pMandatorySpace = let pOptionalSpace = (skipMany (anyOf nonEolWhiteSpace)) "space" +let pEntity = + let isEntityFirstChar c = isLetter c + let isEntityChar c = isLetter c || isDigit c || c = '-' || c = '_' || c = '.' //can't use '/' because that's our ending character + (many1Satisfy2L isEntityFirstChar isEntityChar "entity") .>>? skipString "/" + +let pOptEntity = + opt pEntity + |>> (fun optEntity -> + match optEntity with + | Some entity -> Entity entity + | None -> Default) + let pAccount = - let isAccountFirstChar c = isLetter c - let isAccountChar c = isLetter c || isDigit c || c = '-' || c = '_' || c = '/' || c = ':' || c = '.' - many1Satisfy2L isAccountFirstChar isAccountChar "account" + let isAccountFirstChar c = isLetter c + let isAccountChar c = isLetter c || isDigit c || c = '-' || c = '_' || c = '/' || c = ':' || c = '.' + pipe2 pOptEntity (many1Satisfy2L isAccountFirstChar isAccountChar "account") + (fun entity account -> InputName(entity, account)) let pAudAmount = let isAudAmountFirstChar c = isDigit c || c = '-' || c = '$' || c = '+' @@ -68,12 +81,12 @@ let pVerifyBalance = (pMandatorySpace >>. pAccount) (pMandatorySpace >>. pAmount .>> pOptionalSpace .>> newline) (fun date account amount -> BalanceVerfication { BalanceVerfication.date = date - BalanceVerfication.account = (InputName account) + BalanceVerfication.account = account BalanceVerfication.amount = amount}) let pPosting = pipe2 (pOptionalSpace >>. pAccount) (pMandatorySpace >>. pAmount .>> pOptionalSpace .>> newline) - (fun account amount -> { Posting.account = (InputName account); + (fun account amount -> { Posting.account = account; Posting.amount = amount}) let pPostings = From 07ef7e4eb85d7f96b5bbf5ccfda5045b838a85c1 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Tue, 10 Oct 2017 20:51:17 -0700 Subject: [PATCH 05/28] added function to check a transaction to see if the number and type of entities balance --- src/fsharp/Ledger/Calculations.fs | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/fsharp/Ledger/Calculations.fs b/src/fsharp/Ledger/Calculations.fs index ae840e5..3c6e06d 100644 --- a/src/fsharp/Ledger/Calculations.fs +++ b/src/fsharp/Ledger/Calculations.fs @@ -31,6 +31,10 @@ type DateOrderCheck = | OK | Problem of previous: Transaction * next: Transaction +type EntityVerification = + | EntityVerificationPassed + | UnbalancedEntities of AccountEntity list + /// Check transactions are in date order. Give two problem transactions if not. let checkDateOrder (transactions : Transaction list) = let rec helper (previous : Transaction) (transactions : Transaction list) = @@ -45,6 +49,47 @@ let checkDateOrder (transactions : Transaction list) = | [] -> DateOrderCheck.OK | (t :: tail) -> (helper t tail) +/// This function will check a list of postings for an uneven number of the entities seen within +/// for example: +/// +/// Jacob/Expenses:Motor:Fuel $58.01 +/// Liabilities:Bankwest:Visa $58.01 +/// +/// Will result in the following function result: +/// UnbalancedEntities [(Entity Jacob,true); (Default,true)] +/// +/// Assuming that they do balanced, like so: +/// Expenses:Motor:Fuel $58.01 +/// Liabilities:Bankwest:Visa $58.01 +/// +/// then the function will return EntityVerificationPassed. Perhaps there is a better way to check for the # +/// such as keeping a running total and "modding" (%) by 2 to see if there is an even number of entities +/// I'm keeping it like this because I think the concept of flipping "bits" is neat, although overcomplicated +let verifyEntitiesInPostings (postings:Posting list) = + let errors = postings + |> List.fold (fun (bins:(AccountEntity*bool) list) (elem:Posting) -> + let entity = elem.account.Entity + let bin = bins |> List.tryFind (fst >> (=) entity) + match bin with + | Some b -> bins |> List.map (fun (entity,b) -> (entity,not b)) + | None -> (entity, true) :: bins) [] + |> List.filter (snd >> (=) true) + |> List.map (fun (entity,b) -> entity) + match errors with + | [] -> EntityVerificationPassed + | _ -> UnbalancedEntities errors + +/// Performs verifyEntitiesInPostings of each transaction's postings in the supplied list +/// It will then map the results into a tuple of (Transaction*EntityVerification List) +/// This allows us to refer to the offending transaction in errors, as well as checking each +/// transaction individually rather than checking the ENTIRE file for unbalanced entities +/// this allows for a faster tracking of entity issues +let verifyEntitiesInTransactions (transactions:Transaction list) = + transactions + |> List.map (fun transaction -> + (transaction, match verifyEntitiesInPostings transaction.postings with + | EntityVerificationPassed -> [] + | UnbalancedEntities entities -> entities)) /// Is transaction unbalanced? let balance (t:Transaction) = let signedAmount (p: Posting) = From a4860fed405b8b653781652b64d598667b51b81f Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Tue, 10 Oct 2017 20:51:56 -0700 Subject: [PATCH 06/28] started first stage of entity transition, all tests pass, but full functionality is not yet implemented --- src/fsharp/Ledger/InputTypes.fs | 29 +++++++++++++++++++++++-- src/fsharp/Ledger/InternalTypes.fs | 35 +++++++++++++++--------------- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/fsharp/Ledger/InputTypes.fs b/src/fsharp/Ledger/InputTypes.fs index 32af2c6..6abd55f 100644 --- a/src/fsharp/Ledger/InputTypes.fs +++ b/src/fsharp/Ledger/InputTypes.fs @@ -9,13 +9,38 @@ type Date = string type Description = string -type InputNameAccount = InputName of string +type AccountName = string + +type AccountEntity = + | Default + | TEMPENTITY + | Entity of string + with + member this.AsString = + match this with + | Default -> "Default" + | TEMPENTITY -> "TEMPENTITY" + | Entity e -> e + +type InputNameAccount = InputName of (AccountEntity * AccountName) with member this.AsString = - match this with (InputName x) -> x + match this with (InputName (entity,name)) -> entity.AsString + "/" + name + member this.Name = + match this with (InputName (entity,name)) -> name + member this.Entity = + match this with (InputName (entity,name)) -> entity exception BadAccountNameException of name: InputNameAccount * problem: string +exception EntityMismatch of acc1:InputNameAccount * acc2:InputNameAccount + with + member this.ToString = + "Entity Mismatch: " + this.acc1.Entity.AsString + " <> " + this.acc2.Entity.AsString + "\r\n" + + "Additional Information: " + this.acc1.AsString + " <> " + this.acc2.AsString + +exception BadEntityNameException of name:InputNameAccount * problem: string + type Amount = /// AUD amounts are stored as cents, and converted to dollars on input/output. | AUD of int diff --git a/src/fsharp/Ledger/InternalTypes.fs b/src/fsharp/Ledger/InternalTypes.fs index 184c2e3..4b7dbf6 100644 --- a/src/fsharp/Ledger/InternalTypes.fs +++ b/src/fsharp/Ledger/InternalTypes.fs @@ -23,11 +23,11 @@ let sign (accountType: AccountType) = | Equity -> -1 /// Which of the five basic account types is this? -let accountType (InputName name) = +let accountType (InputName (entity,name)) = let components = name.ToUpper().Split(':') |> Array.toList let root = match components with | root::tail -> root - | _ -> raise (BadAccountNameException((InputName name), "Empty name")) + | _ -> raise (BadAccountNameException((InputName (entity,name)), "Empty name")) match root with | "ASSET" -> Asset | "ASSETS" -> Asset @@ -39,36 +39,33 @@ let accountType (InputName name) = | "EXPENSE" -> Expense | "EXPENSES" -> Expense | "EQUITY" -> Equity - | _ -> raise (BadAccountNameException((InputName name), "Unable to determine account type")) + | _ -> raise (BadAccountNameException((InputName (entity,name)), "Unable to determine account type")) /// Do we have a valid account name? let validAccountName (a:InputNameAccount) = try match (accountType a) with _ -> true with BadAccountNameException(name, problem) -> false - + type CanonicalNameComponent = Canonical of string -type InputNameComponent = Input of string - with - member this.AsInputName = - match this with (Input str) -> (InputName str) type AccountNameComponent = { Canonical: CanonicalNameComponent; - Input: InputNameComponent} + Input: InputNameAccount} type InternalNameAccount = AccountNameComponent list let toInputName (name: InternalNameAccount) : InputNameAccount = + let entity = name.[0].Input.Entity //XXX: There has to be a better way, perhaps we could even do away with this function completely?? let rec helper (name: InternalNameAccount) = match name with | [] -> "" | only::[] -> match (only.Input) with - (Input only) -> only + (InputName (entity,name)) -> name | first::rest -> match (first.Input) with - (Input first) -> first + ":"+(helper rest) - (InputName (helper name)) + (InputName (entity,name)) -> name + ":" + (helper rest) + (InputName (entity, (helper name))) let canonicalRootName name = let accountType = accountType name @@ -82,15 +79,15 @@ let canonicalRootName name = /// Break AccountName into ordered list of components. /// For each level of the account, we produce canonical & input components. /// Checkout out unit test for an example of what this does. -let splitAccountName (InputName name) : InternalNameAccount = +let splitAccountName (InputName (entity,name)) : InternalNameAccount = let components = name.Split(':') |> Array.toList let rec helper (components: string list) = match components with | [] -> [] - | first::rest -> {Canonical = (Canonical (first.ToUpper())); Input = (Input first)} :: (helper rest) + | first::rest -> {Canonical = (Canonical (first.ToUpper())); Input = (InputName (entity,first))} :: (helper rest) match components with - | root::rest -> {Canonical = (canonicalRootName (InputName name)); Input = (Input root)} :: (helper rest) - | [] -> raise (BadAccountNameException((InputName name), "Empty name")) + | root::rest -> {Canonical = (canonicalRootName (InputName (entity,name))); Input = (InputName (entity,root))} :: (helper rest) + | [] -> raise (BadAccountNameException((InputName (entity,name), "Empty name"))) type InputNameAccount @@ -102,8 +99,10 @@ type InputNameAccount | last :: _ -> last | [] -> raise (BadAccountNameException(this, "Empty account name")) -let joinInputNames (InputName parentName) (Input childName) = - (InputName (parentName + ":" + childName)) +let joinInputNames (InputName (parentEntity,parentName)) (InputName (childEntity, childName)) = + if parentEntity <> childEntity then + raise (EntityMismatch ((InputName (parentEntity,parentName)),(InputName (childEntity, childName)))) + else (InputName (parentEntity, parentName + ":" + childName)) /// Canonical parts of splitAccountName From 209f67a16223d8e1790f40c8e81d555ed5494a62 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Tue, 10 Oct 2017 20:55:17 -0700 Subject: [PATCH 07/28] added entity validation (number/type validation ONLY), added untested account parsing to 'running-balance' option --- src/fsharp/Ledger/Program.fs | 48 +++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/src/fsharp/Ledger/Program.fs b/src/fsharp/Ledger/Program.fs index c8165ca..a51145d 100644 --- a/src/fsharp/Ledger/Program.fs +++ b/src/fsharp/Ledger/Program.fs @@ -21,12 +21,14 @@ /// This is a F# rewrite of almost the same thing in python: /// https://github.com/mafm/ledger.py +open FParsec open Parse open Calculations open InputTypes open InternalTypes open Misc open TextOutput +open System open FormatExceptionForDisplay @@ -74,7 +76,7 @@ let validateBalanceAssertions input = assertion.date (Text.fmt assertion.amount) (Text.fmt account.Balance)) - else None) + else None) if errors.Length <> 0 then errors |> List.iter nonFatal @@ -92,6 +94,24 @@ let validate (input: InputFile) = after transaction dated %s\n\ \t%s" next.date next.description prev.date prev.description) + // the side effect of checking for balanced number of entities is single-transaction bills get caught too, though + // a monetary balance check would have the same result + let unbalancedEntities = + transactions + |> verifyEntitiesInTransactions + + match unbalancedEntities with + | [] -> () + | _ -> + unbalancedEntities + |> List.iter (fun (entity,errors) -> + errors + |> List.iter (fun error -> nonFatal (sprintf "Unbalanced number of entity \"%s\" in transaction \"%s %s\"" error.AsString entity.date entity.description))) + fatal "Error in input file - unbalanced number of entities" + + // it's very important that we check for balanced entities first, so that the balance checks will clear without checking the incorrect entities + // this prevents issues like unbalanced entities being considered in the balance. Perhaps this could be refactored later so that "unbalanced" verifies entities? + // it may even need to happen soon, depending on how adding multi-entity support goes for me match (List.filter unbalanced transactions) with | [] -> () | unbalanced -> @@ -161,34 +181,37 @@ let main argv = let input = (parseInputFile inputFileName) (validate input) let destination = ExcelOutput.destination(string arguments.["--excel-output"]) - + if (arguments.["running-balance"].IsTrue) then - let report = (ReportRegister.generateReport input (InputName (string arguments.[""]))) - (ReportRegister.printRegisterReport report) - ExcelOutput.Excel.write(report, destination) - + match run (pAccount |>> fun acc -> acc) (string arguments.[""]) with + | Success(result, _, _) -> + let report = (ReportRegister.generateReport input result) + (ReportRegister.printRegisterReport report) + ExcelOutput.Excel.write(report, destination) + | Failure(errorMessage, _, _) -> fatal errorMessage + if (arguments.["balances"].IsTrue) then let report = (ReportBalances.generateReport input) (ReportBalances.printBalanceReport report) ExcelOutput.Excel.write(report, destination) - + if (arguments.["balances-by-date"].IsTrue) then if dates.Length < 1 then fatal("balances-by-date requires at least one date.") let report = (ReportBalancesByDate.generateReport input dates) (ReportBalancesByDate.printReport report) ExcelOutput.Excel.write(report, destination) - + if (arguments.["chart-of-accounts"].IsTrue) then let report = (ReportChartOfAccounts.generateReport input) (ReportChartOfAccounts.printReport report) ExcelOutput.Excel.write(report, destination) - + if (arguments.["transactions"].IsTrue) then let report = (ReportTransactionList.generateReport input firstDate lastDate) (ReportTransactionList.printReport report) ExcelOutput.Excel.write(report, destination) - + if (arguments.["summary"].IsTrue) then if dates.Length < 1 then fatal("summary requires at least one date.") @@ -198,13 +221,16 @@ let main argv = ExcelOutput.Excel.write((ReportBalancesByDate.generateReport input dates), destination) ExcelOutput.Excel.write((ReportChartOfAccounts.generateReport input), destination) ExcelOutput.Excel.write((ReportTransactionList.generateReport input None lastDate), destination) - + ExcelOutput.save(destination) 0 with | UnableToParseFile(filename, message) -> fatal(sprintf "Error parsing input file '%s' : %s" filename message) -1 + | :? EntityMismatch as e -> + fatal(e.ToString) + -1 | :? System.IO.IOException as e -> fatal(sprintf "IO error: %s" e.Message) -1 From 356de3b6c8f7c8405d4dfdeef610428baca32fc5 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Tue, 10 Oct 2017 20:56:37 -0700 Subject: [PATCH 08/28] !BREAKING CHANGE! Forced entity support, these reports will not work correctly until entity support has been fully implemented --- src/fsharp/Ledger/ReportBalanceSheet.fs | 8 ++++---- src/fsharp/Ledger/ReportBalances.fs | 12 ++++++------ src/fsharp/Ledger/ReportBalancesByDates.fs | 12 ++++++------ src/fsharp/Ledger/ReportChartOfAccounts.fs | 12 ++++++------ src/fsharp/Ledger/ReportProfitAndLoss.fs | 6 +++--- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/fsharp/Ledger/ReportBalanceSheet.fs b/src/fsharp/Ledger/ReportBalanceSheet.fs index 30a6f52..b5988b5 100644 --- a/src/fsharp/Ledger/ReportBalanceSheet.fs +++ b/src/fsharp/Ledger/ReportBalanceSheet.fs @@ -79,9 +79,9 @@ let addLine (name: InputNameAccount) (accounts: DatedAccounts) (dates: Date list let generateReport (input: InputFile) (dates: Date list) = let datedAccounts = (accountsByDate input dates) { Dates = dates; - Lines = (addLine (InputName "Assets") datedAccounts dates - (addLine (InputName "Liabilities") datedAccounts dates - (addLine (InputName "Equity") datedAccounts dates [])))} + Lines = (addLine (InputName(Default, "Assets")) datedAccounts dates + (addLine (InputName(Default, "Liabilities")) datedAccounts dates + (addLine (InputName(Default, "Equity")) datedAccounts dates [])))} let rec printReportLine indent (line : Line) = for balance in line.Amounts.Balances do @@ -89,7 +89,7 @@ let rec printReportLine indent (line : Line) = for i in 1 .. indent do printf " " match line.Account with - (InputName str) -> printf "%s\n" str + (InputName (entity,name)) -> printf "%s\n" name for subLine in line.SubAccounts do printReportLine (indent+2) subLine diff --git a/src/fsharp/Ledger/ReportBalances.fs b/src/fsharp/Ledger/ReportBalances.fs index 04cacaa..ec92a61 100644 --- a/src/fsharp/Ledger/ReportBalances.fs +++ b/src/fsharp/Ledger/ReportBalances.fs @@ -29,7 +29,7 @@ let rec accountBalanceReport (name:InputNameAccount) (a: Account) = -> (accountBalanceReport (joinInputNames name onlyChild.LastName.Input) onlyChild) | _ -> { Account = name; Balance = a.Balance; - SubAccounts = [for account in subAccounts -> (accountBalanceReport account.LastName.Input.AsInputName account)]} + SubAccounts = [for account in subAccounts -> (accountBalanceReport account.LastName.Input account)]} let generateReport (input: InputFile) = let accounts = Accounts(transactions input) @@ -37,11 +37,11 @@ let generateReport (input: InputFile) = match (accounts.find account) with | None -> lines | Some a -> (accountBalanceReport a.FullInputName a)::lines - {lines = (addLine (InputName "Assets") - (addLine (InputName "Liabilities") - (addLine (InputName "Income") - (addLine (InputName "Expenses") - (addLine (InputName "Equity") [])))))} + {lines = (addLine (InputName (Default, "Assets")) + (addLine (InputName (Default, "Liabilities")) + (addLine (InputName (Default, "Income")) + (addLine (InputName (Default, "Expenses")) + (addLine (InputName (Default, "Equity")) [])))))} let rec printBalanceReportLine indent (line : Line) = printf "%s\t" (Text.fmt line.Balance) diff --git a/src/fsharp/Ledger/ReportBalancesByDates.fs b/src/fsharp/Ledger/ReportBalancesByDates.fs index 813bbdd..270ea11 100644 --- a/src/fsharp/Ledger/ReportBalancesByDates.fs +++ b/src/fsharp/Ledger/ReportBalancesByDates.fs @@ -57,7 +57,7 @@ let rec constructReportBalancesByDateLine (accounts : Account option List) (acco | Some account -> account.Balance | None -> zeroAmount) accounts) - { Account = (InputName (Text.fmt accountTree.Name)) + { Account = accountTree.Name.[0].Input //XXX: Ugh, I can't believe I keep doing this. There has to be a better way....... Amounts = { Balances = balances Differences = (differences balances) } @@ -81,11 +81,11 @@ let addLine (name: InputNameAccount) (accounts: DatedAccounts) (dates: Date list let generateReport (input: InputFile) (dates: Date list) = let datedAccounts = (accountsByDate input dates) {Dates = dates; - Lines = (addLine (InputName "Assets") datedAccounts dates - (addLine (InputName "Liabilities") datedAccounts dates - (addLine (InputName "Income") datedAccounts dates - (addLine (InputName "Expenses") datedAccounts dates - (addLine (InputName "Equity") datedAccounts dates [])))))} + Lines = (addLine (InputName(Default, "Assets")) datedAccounts dates + (addLine (InputName(Default, "Liabilities")) datedAccounts dates + (addLine (InputName(Default, "Income")) datedAccounts dates + (addLine (InputName(Default, "Expenses")) datedAccounts dates + (addLine (InputName(Default, "Equity")) datedAccounts dates [])))))} let rec printReportLine indent (line : Line) = for balance in line.Amounts.Balances do diff --git a/src/fsharp/Ledger/ReportChartOfAccounts.fs b/src/fsharp/Ledger/ReportChartOfAccounts.fs index 54212ca..3387bfb 100644 --- a/src/fsharp/Ledger/ReportChartOfAccounts.fs +++ b/src/fsharp/Ledger/ReportChartOfAccounts.fs @@ -28,17 +28,17 @@ let addLine (name: InputNameAccount) (accounts: Accounts) linesSoFar = let generateReport (input: InputFile) = let accounts = (Accounts (transactions input)) - {Lines = (addLine (InputName "Assets") accounts - (addLine (InputName "Liabilities") accounts - (addLine (InputName "Income") accounts - (addLine (InputName "Expenses") accounts - (addLine (InputName "Equity") accounts [])))))} + {Lines = (addLine (InputName(Default, "Assets")) accounts + (addLine (InputName(Default, "Liabilities")) accounts + (addLine (InputName(Default, "Income")) accounts + (addLine (InputName(Default, "Expenses")) accounts + (addLine (InputName(Default, "Equity")) accounts [])))))} let rec printLine indent (line : Line) = for i in 1 .. indent do printf " " match line.Account with - (InputName str) -> printf "%s\n" str + (InputName (entity,name)) -> printf "%s\n" name for subLine in line.SubAccounts do printLine (indent+2) subLine diff --git a/src/fsharp/Ledger/ReportProfitAndLoss.fs b/src/fsharp/Ledger/ReportProfitAndLoss.fs index 020cb59..6dbf871 100644 --- a/src/fsharp/Ledger/ReportProfitAndLoss.fs +++ b/src/fsharp/Ledger/ReportProfitAndLoss.fs @@ -61,7 +61,7 @@ let rec constructReportProfitAndLossLine (accounts : Account option List) (accou | Some account -> account.Balance | None -> zeroAmount) accounts) - { Account = (InputName (Text.fmt accountTree.Name)) + { Account = accountTree.Name.[0].Input //XXX: Ugh, I can't believe I keep doing this. There has to be a better way....... Amounts = { Differences = (differences balances) } SubAccounts = @@ -81,8 +81,8 @@ let addLine (name: InputNameAccount) (accounts: DatedAccounts) (dates: Date list let generateReport (input: InputFile) (dates: Date list) = let datedAccounts = (accountsByDate input dates) {Dates = dates; - Lines = (addLine (InputName "Income") datedAccounts dates - (addLine (InputName "Expenses") datedAccounts dates []))} + Lines = (addLine (InputName(TEMPENTITY, "Income")) datedAccounts dates + (addLine (InputName(TEMPENTITY, "Expenses")) datedAccounts dates []))} let rec printReportLine indent (line : Line) = for difference in line.Amounts.Differences do From 3e158def3b9ea3b0087be3186690a9fe9cc66896 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Tue, 10 Oct 2017 20:56:58 -0700 Subject: [PATCH 09/28] updated tests to pass with current entity support --- src/fsharp/Ledger/InternalTypesTest.fs | 18 +++++----- src/fsharp/Ledger/ParseTest.fs | 46 ++++++++++++------------- src/fsharp/Ledger/RegisterReportTest.fs | 28 +++++++-------- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/fsharp/Ledger/InternalTypesTest.fs b/src/fsharp/Ledger/InternalTypesTest.fs index 6c479a3..5a57fe2 100644 --- a/src/fsharp/Ledger/InternalTypesTest.fs +++ b/src/fsharp/Ledger/InternalTypesTest.fs @@ -12,16 +12,16 @@ open PersistentCollections type ``Test Internal Types`` () = [] member test.``splitAccountName.`` () = - splitAccountName (InputName "Expenses:BankFees:AccountServiceFee") - |> should equal [{Canonical = (Canonical "EXPENSE"); Input = (Input "Expenses");}; - {Canonical = (Canonical "BANKFEES"); Input = (Input "BankFees");}; - {Canonical = (Canonical "ACCOUNTSERVICEFEE"); Input = (Input "AccountServiceFee");}] + splitAccountName (InputName(Entity "TEST_ENTITY", "Expenses:BankFees:AccountServiceFee")) + |> should equal [{Canonical = (Canonical "EXPENSE"); Input = (InputName(Entity "TEST_ENTITY", "Expenses"));}; + {Canonical = (Canonical "BANKFEES"); Input = (InputName(Entity "TEST_ENTITY", "BankFees"));}; + {Canonical = (Canonical "ACCOUNTSERVICEFEE"); Input = (InputName(Entity "TEST_ENTITY", "AccountServiceFee"));}] [] member test.``Book posting to Account.``() = /// I think this might even work ... It did - on the first attempt. // It's a lot easier to write working code in F# than it is in python. - let a = Account(InputName "Assets") - let p = {Posting.account = (InputName "Assets:Bankwest:Cheque"); + let a = Account(InputName(Entity "TEST_ENTITY", "Assets")) + let p = {Posting.account = (InputName(Entity "TEST_ENTITY", "Assets:Bankwest:Cheque")); Posting.amount = AUD 100000;} let t = {Transaction.id = 1; Transaction.date = "1999-12-31"; @@ -41,7 +41,7 @@ type ``Test Internal Types`` () = member test.``Book posting to Accounts.``() = /// And this also seems to work on first attempt. let a = Accounts() - let p = {Posting.account = (InputName "Assets:Bankwest:Cheque"); + let p = {Posting.account = (InputName(Entity "TEST_ENTITY", "Assets:Bankwest:Cheque")); Posting.amount = AUD 100000;} let t = {Transaction.id = 1; Transaction.date = "1999-12-31"; @@ -64,9 +64,9 @@ type ``Test Internal Types`` () = let t = {Transaction.date = "2013-01-01"; description = "I began the year with $1000 in my cheque account."; // NB: Top-level account "Asset" gets created as "ASSET_S_" - postings = [{account = (InputName "Asset:Bankwest:Cheque"); + postings = [{account = (InputName(Entity "TEST_ENTITY", "Asset:Bankwest:Cheque")); amount = AUD 100000;}; - {account = (InputName "Equity:OpeningBalances"); + {account = (InputName(Entity "TEST_ENTITY", "Equity:OpeningBalances")); amount = AUD 100000;}]; id=1} let detail0 = {posting=t.postings.[0];transaction=t} diff --git a/src/fsharp/Ledger/ParseTest.fs b/src/fsharp/Ledger/ParseTest.fs index a6ed80d..312f5fc 100644 --- a/src/fsharp/Ledger/ParseTest.fs +++ b/src/fsharp/Ledger/ParseTest.fs @@ -9,75 +9,75 @@ open InputTypes type ``Test Parsing of transaction text data`` () = [] member test.``parseTransactionData.`` () = - let parse = (parseInputString "2122-22-01 foo\n foo 10AUD\n bar 11\n baz 12\n2012-12-22 foo sss\nacc $10\n") in do + let parse = (parseInputString "2122-22-01 foo\n TEST_ENTITY/foo 10AUD\n TEST_ENTITY/bar 11\n TEST_ENTITY/baz 12\n2012-12-22 foo sss\nTEST_ENTITY/acc $10\n") in do parse |> should equal (ParseSuccess ([Transaction {date = "2122-22-01"; description = "foo"; - postings = [{account = (InputName "foo"); + postings = [{account = (InputName(Entity "TEST_ENTITY", "foo")); amount = AUD 1000;}; - {account = (InputName "bar"); + {account = (InputName(Entity "TEST_ENTITY", "bar")); amount = AUD 1100;}; - {account = (InputName "baz"); + {account = (InputName(Entity "TEST_ENTITY", "baz")); amount = AUD 1200;}]; id=1}; Transaction {date = "2012-12-22"; description = "foo sss"; - postings = [{account = (InputName "acc"); + postings = [{account = (InputName(Entity "TEST_ENTITY","acc")); amount = AUD 1000;}]; id=2}])) [] member test.``Example from readme.md.`` () = let parse = (parseInputString ("""2013-01-01 I began the year with $1000 in my cheque account. - Assets:Bankwest:Cheque $1,000 - Equity:OpeningBalances $1,000 + TEST_ENTITY/Assets:Bankwest:Cheque $1,000 + TEST_ENTITY/Equity:OpeningBalances $1,000 2013-01-05 I bought some groceries and paid using the cheque account. - Expenses:Food:Groceries $98.53 - Assets:Bankwest:Cheque -$98.53 + TEST_ENTITY/Expenses:Food:Groceries $98.53 + TEST_ENTITY/Assets:Bankwest:Cheque -$98.53 2013-01-10 I bought some petrol, and paid using a credit card. - Expenses:Motor:Fuel $58.01 - Liabilities:Bankwest:Visa $58.01 + TEST_ENTITY/Expenses:Motor:Fuel $58.01 + TEST_ENTITY/Liabilities:Bankwest:Visa $58.01 2013-01-15 I paid my electricity bill. - Expenses:Electricity $280.42 - Assets:Bankwest:Cheque -$280.42 + TEST_ENTITY/Expenses:Electricity $280.42 + TEST_ENTITY/Assets:Bankwest:Cheque -$280.42 # I checked my bank statement on the 1st of Feb, and this is what it said. - VERIFY-BALANCE 2013-02-01 Assets:Bankwest:Cheque 621.05""" + "\n")) + VERIFY-BALANCE 2013-02-01 TEST_ENTITY/Assets:Bankwest:Cheque 621.05""" + "\n")) let expected = (ParseSuccess [Transaction {date = "2013-01-01"; description = "I began the year with $1000 in my cheque account."; - postings = [{account = (InputName "Assets:Bankwest:Cheque"); + postings = [{account = (InputName (Entity "TEST_ENTITY","Assets:Bankwest:Cheque")); amount = AUD 100000;}; - {account = (InputName "Equity:OpeningBalances"); + {account = (InputName (Entity "TEST_ENTITY","Equity:OpeningBalances")); amount = AUD 100000;}]; id=1}; BlankLine; Transaction {date = "2013-01-05"; description = "I bought some groceries and paid using the cheque account."; - postings = [{account = (InputName "Expenses:Food:Groceries"); + postings = [{account = (InputName(Entity "TEST_ENTITY", "Expenses:Food:Groceries")); amount = AUD 9853;}; - {account = (InputName "Assets:Bankwest:Cheque"); + {account = (InputName(Entity "TEST_ENTITY", "Assets:Bankwest:Cheque")); amount = AUD -9853;}]; id=2}; BlankLine; Transaction {date = "2013-01-10"; description = "I bought some petrol, and paid using a credit card."; - postings = [{account = (InputName "Expenses:Motor:Fuel"); + postings = [{account = (InputName(Entity "TEST_ENTITY", "Expenses:Motor:Fuel")); amount = AUD 5801;}; - {account = (InputName "Liabilities:Bankwest:Visa"); + {account = (InputName(Entity "TEST_ENTITY", "Liabilities:Bankwest:Visa")); amount = AUD 5801;}]; id=3}; BlankLine; Transaction {date = "2013-01-15"; description = "I paid my electricity bill."; - postings = [{account = (InputName "Expenses:Electricity"); + postings = [{account = (InputName(Entity "TEST_ENTITY", "Expenses:Electricity")); amount = AUD 28042;}; - {account = (InputName "Assets:Bankwest:Cheque"); + {account = (InputName(Entity "TEST_ENTITY", "Assets:Bankwest:Cheque")); amount = AUD -28042;}]; id=4}; BlankLine; Comment " I checked my bank statement on the 1st of Feb, and this is what it said."; BalanceVerfication {date = "2013-02-01"; - account = (InputName "Assets:Bankwest:Cheque"); + account = (InputName(Entity "TEST_ENTITY", "Assets:Bankwest:Cheque")); amount = AUD 62105;}]) in do parse |> should equal expected diff --git a/src/fsharp/Ledger/RegisterReportTest.fs b/src/fsharp/Ledger/RegisterReportTest.fs index c960140..690a4d4 100644 --- a/src/fsharp/Ledger/RegisterReportTest.fs +++ b/src/fsharp/Ledger/RegisterReportTest.fs @@ -15,55 +15,55 @@ type ``Test Register Report`` () = member test.``Test Expenses.`` () = let input = [Transaction {date = "2013-01-01"; description = "I began the year with $1000 in my cheque account."; - postings = [{account = (InputName "Assets:Bankwest:Cheque"); + postings = [{account = (InputName(Entity "TEST_ENTITY", "Assets:Bankwest:Cheque")); amount = AUD 100000;}; - {account = (InputName "Equity:OpeningBalances"); + {account = (InputName(Entity "TEST_ENTITY", "Equity:OpeningBalances")); amount = AUD 100000;}]; id=1}; BlankLine; Transaction {date = "2013-01-05"; description = "I bought some groceries and paid using the cheque account."; - postings = [{account = (InputName "Expenses:Food:Groceries"); + postings = [{account = (InputName(Entity "TEST_ENTITY", "Expenses:Food:Groceries")); amount = AUD 9853;}; - {account = (InputName "Assets:Bankwest:Cheque"); + {account = (InputName(Entity "TEST_ENTITY", "Assets:Bankwest:Cheque")); amount = AUD -9853;}]; id=2}; BlankLine; Transaction {date = "2013-01-10"; description = "I bought some petrol, and paid using a credit card."; - postings = [{account = (InputName "Expenses:Motor:Fuel"); - amount = AUD 5801;}; {account = (InputName "Liabilities:Bankwest:Visa"); + postings = [{account = (InputName(Entity "TEST_ENTITY", "Expenses:Motor:Fuel")); + amount = AUD 5801;}; {account = (InputName(Entity "TEST_ENTITY", "Liabilities:Bankwest:Visa")); amount = AUD 5801;}]; id=3}; BlankLine; Transaction {date = "2013-01-15"; description = "I paid my electricity bill."; - postings = [{account = (InputName "Expenses:Electricity"); + postings = [{account = (InputName(Entity "TEST_ENTITY", "Expenses:Electricity")); amount = AUD 28042;}; - {account = (InputName "Assets:Bankwest:Cheque"); + {account = (InputName(Entity "TEST_ENTITY", "Assets:Bankwest:Cheque")); amount = AUD -28042;}]; id=4}; BlankLine; Comment " I checked my bank statement on the 1st of Feb, and this is what it said."; BalanceVerfication {date = "2013-02-01"; - account = (InputName "Assets:Bankwest:Cheque"); + account = (InputName(Entity "TEST_ENTITY", "Assets:Bankwest:Cheque")); amount = AUD 62105;}] - let expected = {Report.account = (InputName "Expenses"); + let expected = {Report.account = (InputName(Entity "TEST_ENTITY", "Expenses")); from = None; until = None; lines = [{date = "2013-01-05"; amount = AUD 9853; description = "I bought some groceries and paid using the cheque account."; - account = (InputName "Expenses:Food:Groceries"); + account = (InputName(Entity "TEST_ENTITY", "Expenses:Food:Groceries")); balance = AUD 9853;}; {date = "2013-01-10"; amount = AUD 5801; description = "I bought some petrol, and paid using a credit card."; - account = (InputName "Expenses:Motor:Fuel"); + account = (InputName(Entity "TEST_ENTITY", "Expenses:Motor:Fuel")); balance = AUD 15654;}; {date = "2013-01-15"; amount = AUD 28042; description = "I paid my electricity bill."; - account = (InputName "Expenses:Electricity"); + account = (InputName(Entity "TEST_ENTITY", "Expenses:Electricity")); balance = AUD 43696;}];} - (generateReport input (InputName "Expenses")) |> should equal expected + (generateReport input (InputName(Entity "TEST_ENTITY", "Expenses"))) |> should equal expected From 19d870655c20b1230b3cba84d92320200f0e5fc2 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 14 Oct 2017 21:19:15 -0700 Subject: [PATCH 10/28] modified so the VERIFY-BALANCE check would make a bit more sense --- examples/entity_sample.transactions | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/entity_sample.transactions b/examples/entity_sample.transactions index 51bc27e..f3418b8 100644 --- a/examples/entity_sample.transactions +++ b/examples/entity_sample.transactions @@ -1,18 +1,18 @@ 2013-01-01 I began the year with $1000 in my cheque account. - John/Assets:Bankwest:Cheque $1,000 - John/Equity:OpeningBalances $1,000 + Assets:Bankwest:Cheque $1,000 + Equity:OpeningBalances $1,000 2013-01-05 I bought some groceries and paid using the cheque account. Jacob/Expenses:Food:Groceries $98.53 Jacob/Assets:Bankwest:Cheque -$98.53 2013-01-10 I bought some petrol, and paid using a credit card. - Expenses:Motor:Fuel $58.01 - Liabilities:Bankwest:Visa $58.01 + John/Expenses:Motor:Fuel $58.01 + John/Liabilities:Bankwest:Visa $58.01 2013-01-15 I paid my electricity bill. Expenses:Electricity $280.42 Assets:Bankwest:Cheque -$280.42 # I checked my bank statement on the 1st of Feb, and this is what it said. -VERIFY-BALANCE 2013-02-01 Assets:Bankwest:Cheque 621.05 +VERIFY-BALANCE 2013-02-01 Assets:Bankwest:Cheque 719.58 From 577e83effae6c883be17ba2606c4788905d9e5de Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 14 Oct 2017 21:22:59 -0700 Subject: [PATCH 11/28] fixed an issue where passed verifications could still kick errors --- src/fsharp/Ledger/Calculations.fs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/fsharp/Ledger/Calculations.fs b/src/fsharp/Ledger/Calculations.fs index 3c6e06d..32a7a5f 100644 --- a/src/fsharp/Ledger/Calculations.fs +++ b/src/fsharp/Ledger/Calculations.fs @@ -88,8 +88,12 @@ let verifyEntitiesInTransactions (transactions:Transaction list) = transactions |> List.map (fun transaction -> (transaction, match verifyEntitiesInPostings transaction.postings with - | EntityVerificationPassed -> [] + | EntityVerificationPassed -> [] | UnbalancedEntities entities -> entities)) + |> List.filter (snd >> (<>) []) + // F# never ceases to amaze. This bit basically says "filter all items where the second item in a tuple is not an empty list". + // the equivalent is (fun x -> snd x <> []) would achieve the same result + /// Is transaction unbalanced? let balance (t:Transaction) = let signedAmount (p: Posting) = From 34cd171cbc354ef6f5c6e257987f988bce9d5b9e Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 14 Oct 2017 21:23:11 -0700 Subject: [PATCH 12/28] added entity verification --- src/fsharp/Ledger/Calculations.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fsharp/Ledger/Calculations.fs b/src/fsharp/Ledger/Calculations.fs index 32a7a5f..c69d972 100644 --- a/src/fsharp/Ledger/Calculations.fs +++ b/src/fsharp/Ledger/Calculations.fs @@ -115,7 +115,7 @@ let filter (transactions : Transaction list) (first : Date option) (last : Date // Is a a sub-account of b? let isSubAccountOf (a: InputNameAccount) (b: InputNameAccount) = - startsWith (canonicalAccountName a) (canonicalAccountName b) + startsWith (canonicalAccountName a) (canonicalAccountName b) && a.Entity=b.Entity /// XXX: affectedBy(Posting/Transaction) should be a method on AccountName, which should be a class. Do we even need these at all? let postingAffects (p:Posting) (a: InputNameAccount) = From d470d79b9179fc4d00e9f28cce3dcb99af4fc542 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 14 Oct 2017 21:24:16 -0700 Subject: [PATCH 13/28] cosmetic change --- src/fsharp/Ledger/ExcelOutput.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fsharp/Ledger/ExcelOutput.fs b/src/fsharp/Ledger/ExcelOutput.fs index 1a43f18..6f0be13 100644 --- a/src/fsharp/Ledger/ExcelOutput.fs +++ b/src/fsharp/Ledger/ExcelOutput.fs @@ -431,7 +431,7 @@ type Excel = let worksheet = package.Workbook.Worksheets.Add("Balances") (setHeader worksheet.Cells.[1, 1] "Balance") (setHeader worksheet.Cells.[1, 2] "Account") - Excel.writeLines(report.lines, worksheet, 0, 2) |> ignore + Excel.writeLines(report.Lines, worksheet, 0, 2) |> ignore worksheet.View.FreezePanes(2, 1) worksheet.OutLineSummaryBelow <- false From b5b8eb66c1ef2e2a9d31506aebe37d63bfa40ad6 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 14 Oct 2017 21:24:47 -0700 Subject: [PATCH 14/28] added stacktrace to EntityMismatch exception --- src/fsharp/Ledger/InputTypes.fs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fsharp/Ledger/InputTypes.fs b/src/fsharp/Ledger/InputTypes.fs index 6abd55f..cc5ac63 100644 --- a/src/fsharp/Ledger/InputTypes.fs +++ b/src/fsharp/Ledger/InputTypes.fs @@ -37,7 +37,8 @@ exception EntityMismatch of acc1:InputNameAccount * acc2:InputNameAccount with member this.ToString = "Entity Mismatch: " + this.acc1.Entity.AsString + " <> " + this.acc2.Entity.AsString + "\r\n" + - "Additional Information: " + this.acc1.AsString + " <> " + this.acc2.AsString + "Additional Information: " + this.acc1.AsString + " <> " + this.acc2.AsString + "\r\n" + + "Stacktrace: " + this.StackTrace exception BadEntityNameException of name:InputNameAccount * problem: string From 54f76a0927e37cc1327639ebfc74c7c17a58de9e Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 14 Oct 2017 21:26:06 -0700 Subject: [PATCH 15/28] added entity support to CanonicalNameComponent and updated the corresponding functions affected --- src/fsharp/Ledger/InternalTypes.fs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/fsharp/Ledger/InternalTypes.fs b/src/fsharp/Ledger/InternalTypes.fs index 4b7dbf6..e5f7313 100644 --- a/src/fsharp/Ledger/InternalTypes.fs +++ b/src/fsharp/Ledger/InternalTypes.fs @@ -47,7 +47,18 @@ let validAccountName (a:InputNameAccount) = match (accountType a) with _ -> true with BadAccountNameException(name, problem) -> false -type CanonicalNameComponent = Canonical of string +type CanonicalNameComponent = + | Canonical of (AccountEntity * string) + with + member this.Entity = + match this with + | Canonical (entity,canonical) -> entity + member this.CanonicalName = + match this with + | Canonical (entity, canonical) -> canonical + member this.AsInputName = + match this with + | Canonical (entity, canonical) -> InputName(entity, canonical) type AccountNameComponent = { Canonical: CanonicalNameComponent; @@ -70,11 +81,11 @@ let toInputName (name: InternalNameAccount) : InputNameAccount = let canonicalRootName name = let accountType = accountType name (Canonical (match accountType with - | Asset -> "ASSETS" - | Liability -> "LIABILITY" - | Income -> "INCOME" - | Expense -> "EXPENSE" - | Equity -> "EQUITY")) + | Asset -> (name.Entity, "ASSETS") + | Liability -> (name.Entity, "LIABILITY") + | Income -> (name.Entity, "INCOME") + | Expense -> (name.Entity, "EXPENSE") + | Equity -> (name.Entity, "EQUITY"))) /// Break AccountName into ordered list of components. /// For each level of the account, we produce canonical & input components. @@ -84,7 +95,7 @@ let splitAccountName (InputName (entity,name)) : InternalNameAccount = let rec helper (components: string list) = match components with | [] -> [] - | first::rest -> {Canonical = (Canonical (first.ToUpper())); Input = (InputName (entity,first))} :: (helper rest) + | first::rest -> {Canonical = (Canonical (entity, first.ToUpper())); Input = (InputName (entity,first))} :: (helper rest) match components with | root::rest -> {Canonical = (canonicalRootName (InputName (entity,name))); Input = (InputName (entity,root))} :: (helper rest) | [] -> raise (BadAccountNameException((InputName (entity,name), "Empty name"))) From 971888c04754c1161b79c349cbaaea35d7626023 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 14 Oct 2017 21:26:48 -0700 Subject: [PATCH 16/28] updated to reflect changes in Canonical type --- src/fsharp/Ledger/InternalTypesTest.fs | 52 +++++++++++++------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/fsharp/Ledger/InternalTypesTest.fs b/src/fsharp/Ledger/InternalTypesTest.fs index 5a57fe2..f22cb51 100644 --- a/src/fsharp/Ledger/InternalTypesTest.fs +++ b/src/fsharp/Ledger/InternalTypesTest.fs @@ -13,9 +13,9 @@ type ``Test Internal Types`` () = [] member test.``splitAccountName.`` () = splitAccountName (InputName(Entity "TEST_ENTITY", "Expenses:BankFees:AccountServiceFee")) - |> should equal [{Canonical = (Canonical "EXPENSE"); Input = (InputName(Entity "TEST_ENTITY", "Expenses"));}; - {Canonical = (Canonical "BANKFEES"); Input = (InputName(Entity "TEST_ENTITY", "BankFees"));}; - {Canonical = (Canonical "ACCOUNTSERVICEFEE"); Input = (InputName(Entity "TEST_ENTITY", "AccountServiceFee"));}] + |> should equal [{Canonical = (Canonical (Entity "TEST_ENTITY", "EXPENSE")); Input = (InputName(Entity "TEST_ENTITY", "Expenses"));}; + {Canonical = (Canonical (Entity "TEST_ENTITY", "BANKFEES")); Input = (InputName(Entity "TEST_ENTITY", "BankFees"));}; + {Canonical = (Canonical (Entity "TEST_ENTITY", "ACCOUNTSERVICEFEE")); Input = (InputName(Entity "TEST_ENTITY", "AccountServiceFee"));}] [] member test.``Book posting to Account.``() = /// I think this might even work ... It did - on the first attempt. @@ -29,14 +29,14 @@ type ``Test Internal Types`` () = Transaction.description = "dummy"} let detail = {posting=p;transaction=t} let a2 = a.Book(p, t, (List.tail (splitAccountName p.account))) - a2.SubAccounts.ContainsKey(Canonical "BANKWEST") |> should be True - a2.SubAccounts.[Canonical "BANKWEST"].SubAccounts.ContainsKey(Canonical "CHEQUE") |> should be True + a2.SubAccounts.ContainsKey(Canonical (Entity "TEST_ENTITY", "BANKWEST")) |> should be True + a2.SubAccounts.[Canonical (Entity "TEST_ENTITY", "BANKWEST")].SubAccounts.ContainsKey(Canonical (Entity "TEST_ENTITY", "CHEQUE")) |> should be True a2.Balance |> should equal (AUD 100000) - a2.SubAccounts.[Canonical "BANKWEST"].Balance |> should equal (AUD 100000) - a2.SubAccounts.[Canonical "BANKWEST"].SubAccounts.[Canonical "CHEQUE"].Balance |> should equal (AUD 100000) + a2.SubAccounts.[Canonical (Entity "TEST_ENTITY", "BANKWEST")].Balance |> should equal (AUD 100000) + a2.SubAccounts.[Canonical (Entity "TEST_ENTITY", "BANKWEST")].SubAccounts.[Canonical(Entity "TEST_ENTITY", "CHEQUE")].Balance |> should equal (AUD 100000) a2.Postings |> should equal PersistentQueue.Empty - a2.SubAccounts.[Canonical "BANKWEST"].Postings |> should equal PersistentQueue.Empty - a2.SubAccounts.[Canonical "BANKWEST"].SubAccounts.[Canonical "CHEQUE"].Postings |> should equal (PersistentQueue.Empty.Enqueue detail) + a2.SubAccounts.[Canonical (Entity "TEST_ENTITY", "BANKWEST")].Postings |> should equal PersistentQueue.Empty + a2.SubAccounts.[Canonical (Entity "TEST_ENTITY", "BANKWEST")].SubAccounts.[Canonical (Entity "TEST_ENTITY", "CHEQUE")].Postings |> should equal (PersistentQueue.Empty.Enqueue detail) [] member test.``Book posting to Accounts.``() = /// And this also seems to work on first attempt. @@ -49,15 +49,15 @@ type ``Test Internal Types`` () = Transaction.description = "dummy"} let detail = {posting=p;transaction=t} let a2 = a.Book(p, t) - a2.Accounts.ContainsKey(Canonical "ASSETS") |> should be True - a2.Accounts.[Canonical "ASSETS"].SubAccounts.ContainsKey(Canonical "BANKWEST") |> should be True - a2.Accounts.[Canonical "ASSETS"].SubAccounts.[Canonical "BANKWEST"].SubAccounts.ContainsKey(Canonical "CHEQUE") |> should be True - a2.Accounts.[Canonical "ASSETS"].Balance |> should equal (AUD 100000) - a2.Accounts.[Canonical "ASSETS"].SubAccounts.[Canonical "BANKWEST"].Balance |> should equal (AUD 100000) - a2.Accounts.[Canonical "ASSETS"].SubAccounts.[Canonical "BANKWEST"].SubAccounts.[Canonical "CHEQUE"].Balance |> should equal (AUD 100000) - a2.Accounts.[Canonical "ASSETS"].Postings |> should equal PersistentQueue.Empty - a2.Accounts.[Canonical"ASSETS"].SubAccounts.[Canonical "BANKWEST"].Postings |> should equal PersistentQueue.Empty - a2.Accounts.[Canonical"ASSETS"].SubAccounts.[Canonical "BANKWEST"].SubAccounts.[Canonical "CHEQUE"].Postings |> should equal (PersistentQueue.Empty.Enqueue detail) + a2.Accounts.ContainsKey(Canonical (Entity "TEST_ENTITY", "ASSETS")) |> should be True + a2.Accounts.[Canonical (Entity "TEST_ENTITY", "ASSETS")].SubAccounts.ContainsKey(Canonical (Entity "TEST_ENTITY", "BANKWEST")) |> should be True + a2.Accounts.[Canonical (Entity "TEST_ENTITY", "ASSETS")].SubAccounts.[Canonical (Entity "TEST_ENTITY", "BANKWEST")].SubAccounts.ContainsKey(Canonical(Entity "TEST_ENTITY", "CHEQUE")) |> should be True + a2.Accounts.[Canonical (Entity "TEST_ENTITY", "ASSETS")].Balance |> should equal (AUD 100000) + a2.Accounts.[Canonical (Entity "TEST_ENTITY", "ASSETS")].SubAccounts.[Canonical (Entity "TEST_ENTITY", "BANKWEST")].Balance |> should equal (AUD 100000) + a2.Accounts.[Canonical (Entity "TEST_ENTITY", "ASSETS")].SubAccounts.[Canonical (Entity "TEST_ENTITY", "BANKWEST")].SubAccounts.[Canonical (Entity "TEST_ENTITY", "CHEQUE")].Balance |> should equal (AUD 100000) + a2.Accounts.[Canonical (Entity "TEST_ENTITY", "ASSETS")].Postings |> should equal PersistentQueue.Empty + a2.Accounts.[Canonical (Entity "TEST_ENTITY", "ASSETS")].SubAccounts.[Canonical (Entity "TEST_ENTITY", "BANKWEST")].Postings |> should equal PersistentQueue.Empty + a2.Accounts.[Canonical (Entity "TEST_ENTITY", "ASSETS")].SubAccounts.[Canonical (Entity "TEST_ENTITY", "BANKWEST")].SubAccounts.[Canonical (Entity "TEST_ENTITY", "CHEQUE")].Postings |> should equal (PersistentQueue.Empty.Enqueue detail) [] member test.``Book transaction to Accounts.``() = // Maybe this is also right first time. @@ -73,11 +73,11 @@ type ``Test Internal Types`` () = let detail1 = {posting=t.postings.[1];transaction=t} let a = Accounts() let a2 = a.Book(t) - a2.Accounts.ContainsKey(Canonical "ASSETS") |> should be True - a2.Accounts.ContainsKey(Canonical "EQUITY") |> should be True - a2.Accounts.[Canonical "ASSETS"].Balance |> should equal (AUD 100000) - a2.Accounts.[Canonical "EQUITY"].Balance |> should equal (AUD 100000) - a2.Accounts.[Canonical "ASSETS"].Postings |> should equal (PersistentQueue.Empty) - a2.Accounts.[Canonical "EQUITY"].Postings |> should equal (PersistentQueue.Empty) - a2.Accounts.[Canonical "ASSETS"].SubAccounts.[Canonical "BANKWEST"].SubAccounts.[Canonical "CHEQUE"].Postings |> should equal (PersistentQueue.Empty.Enqueue detail0) - a2.Accounts.[Canonical "EQUITY"].SubAccounts.[Canonical "OPENINGBALANCES"].Postings |> should equal (PersistentQueue.Empty.Enqueue detail1) + a2.Accounts.ContainsKey(Canonical (Entity "TEST_ENTITY", "ASSETS")) |> should be True + a2.Accounts.ContainsKey(Canonical (Entity "TEST_ENTITY", "EQUITY")) |> should be True + a2.Accounts.[Canonical (Entity "TEST_ENTITY", "ASSETS")].Balance |> should equal (AUD 100000) + a2.Accounts.[Canonical (Entity "TEST_ENTITY", "EQUITY")].Balance |> should equal (AUD 100000) + a2.Accounts.[Canonical (Entity "TEST_ENTITY", "ASSETS")].Postings |> should equal (PersistentQueue.Empty) + a2.Accounts.[Canonical (Entity "TEST_ENTITY", "EQUITY")].Postings |> should equal (PersistentQueue.Empty) + a2.Accounts.[Canonical (Entity "TEST_ENTITY", "ASSETS")].SubAccounts.[Canonical (Entity "TEST_ENTITY", "BANKWEST")].SubAccounts.[Canonical (Entity "TEST_ENTITY", "CHEQUE")].Postings |> should equal (PersistentQueue.Empty.Enqueue detail0) + a2.Accounts.[Canonical (Entity "TEST_ENTITY", "EQUITY")].SubAccounts.[Canonical (Entity "TEST_ENTITY", "OPENINGBALANCES")].Postings |> should equal (PersistentQueue.Empty.Enqueue detail1) From 543554984a806cbaacf1a03352723a83cdd52ccf Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 14 Oct 2017 21:27:36 -0700 Subject: [PATCH 17/28] added "collapse" function to collapse list of tuples into (key*value[]) pairs --- src/fsharp/Ledger/Misc.fs | 63 ++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/src/fsharp/Ledger/Misc.fs b/src/fsharp/Ledger/Misc.fs index 35e45dd..5e80b8c 100644 --- a/src/fsharp/Ledger/Misc.fs +++ b/src/fsharp/Ledger/Misc.fs @@ -1,22 +1,43 @@ -/// Stuff that should be built into the language, but I couldn't find. +/// Stuff that should be built into the language, but I couldn't find. + +module Misc -module Misc - -/// Remove occurences of chars from s -// http://rosettacode.org/wiki/Strip_a_set_of_characters_from_a_string#F.23 -let stripChars (chars:string) (s:string) = - Array.fold (fun (s:string) c -> s.Replace(c.ToString(),"")) - s (chars.ToCharArray()) - -/// Does list a start with list b? -let rec startsWith<'X when 'X: equality> (a :'X list) (b :'X list) = - match b with - | [] -> true - | B::Bs -> match a with - | A::As -> (A=B) && (startsWith As Bs) - | _ -> false - -// Is this actually useful anywhere? Can I use this in practice? -type NonEmptyList<'t> = - | One of 't - | More of first:'t * rest:NonEmptyList<'t> \ No newline at end of file +/// Remove occurences of chars from s +// http://rosettacode.org/wiki/Strip_a_set_of_characters_from_a_string#F.23 +let stripChars (chars:string) (s:string) = + Array.fold (fun (s:string) c -> s.Replace(c.ToString(),"")) + s (chars.ToCharArray()) + +/// Does list a start with list b? +let rec startsWith<'X when 'X: equality> (a :'X list) (b :'X list) = + match b with + | [] -> true + | B::Bs -> match a with + | A::As -> (A=B) && (startsWith As Bs) + | _ -> false + +// Is this actually useful anywhere? Can I use this in practice? +type NonEmptyList<'t> = + | One of 't + | More of first:'t * rest:NonEmptyList<'t> + +// Collapse functions. Basically collapses a list of tuples to a dictionary. I have chosen to return a list of tuples in the format (key*values[]) instead. +module List = + let collapse (kv:('a*'b) List) = + let data1, data2 = kv |> List.unzip + let keys = data1 |> Seq.distinct |> List.ofSeq + + keys + |> List.map (fun x -> (x, kv + |> List.filter (fun (k,v) -> k=x) + |> List.map snd)) + +module Array = + let collapse(kv:('a*'b)[]) = + let data1, data2 = kv |> Array.unzip + let keys = data1 |> Seq.distinct |> Array.ofSeq //there's a natice Array.distinct in newer versions of F#, but this project is targeting an older version + + keys + |> Array.map (fun x -> (x, kv + |> Array.filter (fun (k,v) -> k=x) + |> Array.map snd)) \ No newline at end of file From 87d1e6973d72d1a568b36e939cc358b24272c96d Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 14 Oct 2017 21:32:03 -0700 Subject: [PATCH 18/28] updated demo to use entity file --- src/fsharp/Ledger/Program.fs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/fsharp/Ledger/Program.fs b/src/fsharp/Ledger/Program.fs index a51145d..53848ca 100644 --- a/src/fsharp/Ledger/Program.fs +++ b/src/fsharp/Ledger/Program.fs @@ -119,15 +119,16 @@ let validate (input: InputFile) = (nonFatal (sprintf "Imbalance of %s in transaction dated %s (%s)." (Text.fmt (absAmount (balance t))) t.date t.description)) (fatal "Error in input file - unbalanced transactions.") + (validateBalanceAssertions input) let demo () = try let timer = new System.Diagnostics.Stopwatch() - let input = parseInputFile "..\..\..\..\..\Examples\sample.transactions" in do + let input = parseInputFile "..\..\..\..\..\Examples\entity_sample.transactions" in do (validate input) printfn "Elapsed Time: %i ms.\n" timer.ElapsedMilliseconds - let report = (ReportRegister.generateReport input (InputName "Expenses")) in do + let report = (ReportRegister.generateReport input (InputName (Entity "TEST_ENTITY","Expenses"))) in do printf "Elapsed Time: %i ms.\n" timer.ElapsedMilliseconds (printf "\nDEMO: EXPENSES REGISTER\n") (ReportRegister.printRegisterReport report) @@ -174,6 +175,8 @@ let usage = """Ledger.fs: simple command-line double-entry accounting. let main argv = try let arguments = DocoptNet.Docopt().Apply(usage, argv, exit=true) + //let testargv = [|"..\..\..\..\..\Examples\entity_sample.transactions"; "transactions"; "2013-01-05"; "2013-01-10"|] + //let arguments = DocoptNet.Docopt().Apply(usage, testargv) let inputFileName = (string arguments.[""]) let dates = ([for d in arguments.[""].AsList -> (string d)] |> List.sort) let firstDate = match (string arguments.[""]) with | "" -> None | date -> Some date From e91b9a2f2f8425dc17a9d370c86e88fd4018b16a Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 14 Oct 2017 21:32:28 -0700 Subject: [PATCH 19/28] added entity support to all reports --- src/fsharp/Ledger/ReportBalances.fs | 13 +++++------ src/fsharp/Ledger/ReportBalancesByDates.fs | 16 ++++++++----- src/fsharp/Ledger/ReportChartOfAccounts.fs | 27 ++++++++++++++-------- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/fsharp/Ledger/ReportBalances.fs b/src/fsharp/Ledger/ReportBalances.fs index ec92a61..f92e46e 100644 --- a/src/fsharp/Ledger/ReportBalances.fs +++ b/src/fsharp/Ledger/ReportBalances.fs @@ -17,7 +17,7 @@ type Line = Balance : Amount SubAccounts : Line list } -type Report = {lines: Line list} +type Report = {Lines: Line list} let rec accountBalanceReport (name:InputNameAccount) (a: Account) = let subAccounts = [for KeyValue(name, account) in (a.SubAccounts|>Seq.sortBy (fun (KeyValue(k,_)) -> k) ) -> account] @@ -37,11 +37,10 @@ let generateReport (input: InputFile) = match (accounts.find account) with | None -> lines | Some a -> (accountBalanceReport a.FullInputName a)::lines - {lines = (addLine (InputName (Default, "Assets")) - (addLine (InputName (Default, "Liabilities")) - (addLine (InputName (Default, "Income")) - (addLine (InputName (Default, "Expenses")) - (addLine (InputName (Default, "Equity")) [])))))} + {Lines=([for account in accounts.Accounts -> + account.Key.AsInputName] + |> List.fold (fun (acc:Line list) (elem:InputNameAccount) -> + addLine(elem) acc) [])} let rec printBalanceReportLine indent (line : Line) = printf "%s\t" (Text.fmt line.Balance) @@ -53,5 +52,5 @@ let rec printBalanceReportLine indent (line : Line) = let printBalanceReport report = printf "Balance\tAccount\n-------\t-------\n" - for line in report.lines do + for line in report.Lines do printBalanceReportLine 0 line diff --git a/src/fsharp/Ledger/ReportBalancesByDates.fs b/src/fsharp/Ledger/ReportBalancesByDates.fs index 270ea11..5139719 100644 --- a/src/fsharp/Ledger/ReportBalancesByDates.fs +++ b/src/fsharp/Ledger/ReportBalancesByDates.fs @@ -67,7 +67,7 @@ let rec constructReportBalancesByDateLine (accounts : Account option List) (acco (constructReportBalancesByDateLine childAccounts child) ] Postings = accountTree.Postings } -let addLine (name: InputNameAccount) (accounts: DatedAccounts) (dates: Date list) linesSoFar = +let addLine (accounts: DatedAccounts) (dates: Date list) (name: InputNameAccount) linesSoFar = let lastDate = (List.max dates) let finalAccounts = accounts.[lastDate] in match finalAccounts.find(name) with @@ -80,12 +80,16 @@ let addLine (name: InputNameAccount) (accounts: DatedAccounts) (dates: Date list let generateReport (input: InputFile) (dates: Date list) = let datedAccounts = (accountsByDate input dates) + let addLine = addLine datedAccounts dates + let lines = [for a in datedAccounts -> + [for b in a.Value.Accounts -> + b.Value.FullInputName]] + |> List.collect id + |> List.fold (fun (acc:Line list) (elem:InputNameAccount) -> + addLine(elem) acc) [] + {Dates = dates; - Lines = (addLine (InputName(Default, "Assets")) datedAccounts dates - (addLine (InputName(Default, "Liabilities")) datedAccounts dates - (addLine (InputName(Default, "Income")) datedAccounts dates - (addLine (InputName(Default, "Expenses")) datedAccounts dates - (addLine (InputName(Default, "Equity")) datedAccounts dates [])))))} + Lines = lines} let rec printReportLine indent (line : Line) = for balance in line.Amounts.Balances do diff --git a/src/fsharp/Ledger/ReportChartOfAccounts.fs b/src/fsharp/Ledger/ReportChartOfAccounts.fs index 3387bfb..36fcda1 100644 --- a/src/fsharp/Ledger/ReportChartOfAccounts.fs +++ b/src/fsharp/Ledger/ReportChartOfAccounts.fs @@ -21,18 +21,18 @@ let rec constructLine (account: Account) = SubAccounts = [for child in subAccounts -> (constructLine child)]} -let addLine (name: InputNameAccount) (accounts: Accounts) linesSoFar = +let addLine (accounts: Accounts) (name: InputNameAccount) linesSoFar = match accounts.find(name) with | Some account -> ((constructLine account) :: linesSoFar) | None -> linesSoFar let generateReport (input: InputFile) = let accounts = (Accounts (transactions input)) - {Lines = (addLine (InputName(Default, "Assets")) accounts - (addLine (InputName(Default, "Liabilities")) accounts - (addLine (InputName(Default, "Income")) accounts - (addLine (InputName(Default, "Expenses")) accounts - (addLine (InputName(Default, "Equity")) accounts [])))))} + let addLine = addLine accounts //partial application, one of the greatest features in F#, allows for cleaner code when we're simply passing a single, static variable + {Lines=([for account in accounts.Accounts -> + account.Key.AsInputName] + |> List.fold (fun (acc:Line list) (elem:InputNameAccount) -> + addLine elem acc) [])} let rec printLine indent (line : Line) = for i in 1 .. indent do @@ -45,6 +45,15 @@ let rec printLine indent (line : Line) = let printReport report = (* Balance/Change headings line *) printf "Account\n" - printf "-------\n" - for line in report.Lines do - printLine 0 line \ No newline at end of file + printf "-------\n" + + let collapsed = + report.Lines + |> List.map (fun l -> (l.Account.Entity, l)) + |> List.collapse + + for (key,lines) in collapsed do + printfn "%s/" key.AsString + for line in lines do + printLine 1 line + printfn "" \ No newline at end of file From be31edc1cc038d30c51ffdc80312909f6c605010 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 14 Oct 2017 22:11:43 -0700 Subject: [PATCH 20/28] fixed formatting of account names in ReportBalancesByDates report. --- src/fsharp/Ledger/ReportBalancesByDates.fs | 4 ++-- src/fsharp/Ledger/TextOutput.fs | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/fsharp/Ledger/ReportBalancesByDates.fs b/src/fsharp/Ledger/ReportBalancesByDates.fs index 5139719..1318da9 100644 --- a/src/fsharp/Ledger/ReportBalancesByDates.fs +++ b/src/fsharp/Ledger/ReportBalancesByDates.fs @@ -57,7 +57,7 @@ let rec constructReportBalancesByDateLine (accounts : Account option List) (acco | Some account -> account.Balance | None -> zeroAmount) accounts) - { Account = accountTree.Name.[0].Input //XXX: Ugh, I can't believe I keep doing this. There has to be a better way....... + { Account = InputName (accountTree.Name.[0].Input.Entity, Text.fmt accountTree.Name) //XXX: Ugh, I can't believe I keep doing this. There has to be a better way....... Amounts = { Balances = balances Differences = (differences balances) } @@ -98,7 +98,7 @@ let rec printReportLine indent (line : Line) = printf "%s\t" (Text.fmt difference) for i in 1 .. indent do printf " " - printf "%s\n" line.Account.AsString + printf "%s\n" line.Account.Name for subLine in line.SubAccounts do printReportLine (indent+2) subLine diff --git a/src/fsharp/Ledger/TextOutput.fs b/src/fsharp/Ledger/TextOutput.fs index 5127c2f..323b288 100644 --- a/src/fsharp/Ledger/TextOutput.fs +++ b/src/fsharp/Ledger/TextOutput.fs @@ -11,14 +11,16 @@ open InternalTypes type Text = static member fmt (x:InternalNameAccount) = let entity = x.[0].Input.Entity.AsString //XXX: There has to be a better way, perhaps InternalNameAccount will have Entity as a top level field? - (sprintf "%s/" entity) + (match x with - | last::[] -> - match last.Input with - (InputName (entity, name)) -> (sprintf "%s" name) - | first::rest -> - match first.Input with - (InputName (entity, name)) -> (sprintf "%s:%s" name (Text.fmt rest)) - | [] -> raise EmptyAccountNameComponentsException) + let rec helper (y:InternalNameAccount) = + match y with + | last::[] -> + match last.Input with + (InputName (entity, name)) -> (sprintf "%s" name) + | first::rest -> + match first.Input with + (InputName (entity, name)) -> (sprintf "%s:%s" name (helper rest)) + | [] -> raise EmptyAccountNameComponentsException + (sprintf "%s/" entity) + (helper x) static member fmt (x: Amount) = match x with From 4d655f57af82d2eff16f88b4111f201bec273aa9 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Fri, 20 Oct 2017 16:58:22 -0700 Subject: [PATCH 21/28] removed global transactionId state, redefined run for convenience --- src/fsharp/Ledger/Parse.fs | 46 +++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/fsharp/Ledger/Parse.fs b/src/fsharp/Ledger/Parse.fs index d03f909..0482aac 100644 --- a/src/fsharp/Ledger/Parse.fs +++ b/src/fsharp/Ledger/Parse.fs @@ -6,19 +6,28 @@ open Misc open FParsec -// XXX: Yuck! Can I do this without global state! -// Yes: http://www.quanttec.com/fparsec/users-guide/parsing-with-user-state.html -let mutable transactionId = 0 -let nextTransactionId () = - transactionId <- transactionId + 1 - transactionId -let resetTransactionId () = - transactionId <- 0 +type UserState = {TransactionId:int} + with + static member Default = + {TransactionId=0} + + member this.NextId (stream:CharStream) = + let next = {TransactionId=this.TransactionId+1} + stream.UserState <- next //there is no escaping mutables for UserState, unfortunately + next.TransactionId + + member this.ResetId (stream:CharStream) = + let next = {TransactionId=0} + stream.UserState <- next + next.TransactionId type ParseResult = | ParseError of string | ParseSuccess of InputFile +let run parser str = + runParserOnString parser UserState.Default "" str + let nonEolWhiteSpace = " \t" // At least one space let pMandatorySpace = @@ -93,14 +102,17 @@ let pPostings = (many1 (attempt pPosting)) let pTransaction = - pipe3 pDate - (pMandatorySpace >>. (restOfLine false) .>> newline) - pPostings - (fun date description postings -> - Transaction { Transaction.date = date; - Transaction.description = description; - Transaction.postings = postings - Transaction.id = nextTransactionId()}) + fun (stream:CharStream) -> + let p = pipe3 + pDate + (pMandatorySpace >>. (restOfLine false) .>> newline) + pPostings + (fun date description postings -> + Transaction { Transaction.date = date; + Transaction.description = description; + Transaction.postings = postings + Transaction.id = stream.UserState.NextId stream}) + p stream let pInput = pOptionalSpace >>. (pCommentLine <|> pBlankLine <|> pVerifyBalance <|> pTransaction) @@ -114,8 +126,6 @@ let pInputFile = // Top-level parsing routine(s). let parseInputString str = - resetTransactionId() // XXX: Yuck! Can I do this without global state! - // Yes: http://www.quanttec.com/fparsec/users-guide/parsing-with-user-state.html match run pInputFile str with | Success(result, _, _) -> ParseSuccess(result) | Failure(errorMessage, _, _) -> ParseError(errorMessage) From cd7636fdb318f30224ec8cceb45894678cf91499 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Fri, 20 Oct 2017 16:59:01 -0700 Subject: [PATCH 22/28] removed unnecessary calls to Array.OfList by insantiating an array instead of list --- src/fsharp/Ledger/Parse.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fsharp/Ledger/Parse.fs b/src/fsharp/Ledger/Parse.fs index 0482aac..27d3878 100644 --- a/src/fsharp/Ledger/Parse.fs +++ b/src/fsharp/Ledger/Parse.fs @@ -66,11 +66,11 @@ let pAmount = pAudAmount let pYear = - (pipe4 digit digit digit digit (fun a b c d -> System.String.Concat(Array.ofList([a;b;c;d])))) "year" + (pipe4 digit digit digit digit (fun a b c d -> System.String.Concat([|a;b;c;d|]))) "year" let pMonth = - (pipe2 digit digit (fun a b -> System.String.Concat(Array.ofList([a;b])))) "month" + (pipe2 digit digit (fun a b -> System.String.Concat([|a;b|]))) "month" let pDay = - (pipe2 digit digit (fun a b -> System.String.Concat(Array.ofList([a;b])))) "day" + (pipe2 digit digit (fun a b -> System.String.Concat([|a;b|]))) "day" let pDate = (pipe3 (pYear .>> (pchar '-')) (pMonth .>> (pchar '-')) From 7f1fcc511cc20c63940cc39cb8dad8f70717a277 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 21 Oct 2017 20:55:26 -0700 Subject: [PATCH 23/28] fixed an issue where sorting sub accoutns by name would take entity into consideration --- src/fsharp/Ledger/InternalTypes.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fsharp/Ledger/InternalTypes.fs b/src/fsharp/Ledger/InternalTypes.fs index e5f7313..469064c 100644 --- a/src/fsharp/Ledger/InternalTypes.fs +++ b/src/fsharp/Ledger/InternalTypes.fs @@ -224,7 +224,7 @@ type Account = struct else None member this.SubAccountsOrderedByInputName = - let namesAndAccounts = (this.SubAccounts |> Seq.sortBy (fun (KeyValue(_,account)) -> account.LastName.Input)) + let namesAndAccounts = (this.SubAccounts |> Seq.sortBy (fun (KeyValue(_,account)) -> account.LastName.Input.Name)) [for KeyValue(_, account) in namesAndAccounts -> account] end From 4086df473ef4fc7538aa9c5cafae7fdadcbdcf0c Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 21 Oct 2017 20:56:53 -0700 Subject: [PATCH 24/28] Removed TEMPENTITY as it was simply a "TODO" that made the code compile --- src/fsharp/Ledger/InputTypes.fs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/fsharp/Ledger/InputTypes.fs b/src/fsharp/Ledger/InputTypes.fs index cc5ac63..b698db5 100644 --- a/src/fsharp/Ledger/InputTypes.fs +++ b/src/fsharp/Ledger/InputTypes.fs @@ -13,13 +13,11 @@ type AccountName = string type AccountEntity = | Default - | TEMPENTITY | Entity of string with member this.AsString = match this with | Default -> "Default" - | TEMPENTITY -> "TEMPENTITY" | Entity e -> e type InputNameAccount = InputName of (AccountEntity * AccountName) From 65863f6cdf6f64b730f10f37e1a26369f9a6c7ed Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 21 Oct 2017 20:58:59 -0700 Subject: [PATCH 25/28] reports now reflect entities properly and fixed an issue where accounts would appear multiple times in a single report --- src/fsharp/Ledger/ReportBalancesByDates.fs | 12 +++++------- src/fsharp/Ledger/ReportChartOfAccounts.fs | 17 +++++++++-------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/fsharp/Ledger/ReportBalancesByDates.fs b/src/fsharp/Ledger/ReportBalancesByDates.fs index 1318da9..309b94f 100644 --- a/src/fsharp/Ledger/ReportBalancesByDates.fs +++ b/src/fsharp/Ledger/ReportBalancesByDates.fs @@ -81,12 +81,10 @@ let addLine (accounts: DatedAccounts) (dates: Date list) (name: InputNameAccount let generateReport (input: InputFile) (dates: Date list) = let datedAccounts = (accountsByDate input dates) let addLine = addLine datedAccounts dates - let lines = [for a in datedAccounts -> - [for b in a.Value.Accounts -> - b.Value.FullInputName]] - |> List.collect id - |> List.fold (fun (acc:Line list) (elem:InputNameAccount) -> - addLine(elem) acc) [] + let lines = [for account in datedAccounts.[dates|>List.max].Accounts -> account.Value.FullInputName] + |> List.sortBy (fun x -> x.Entity) + |> List.fold (fun (acc:Line list) (elem:InputNameAccount) -> + addLine(elem) acc) [] {Dates = dates; Lines = lines} @@ -98,7 +96,7 @@ let rec printReportLine indent (line : Line) = printf "%s\t" (Text.fmt difference) for i in 1 .. indent do printf " " - printf "%s\n" line.Account.Name + printf "%s/%s\n" line.Account.Entity.AsString line.Account.Name for subLine in line.SubAccounts do printReportLine (indent+2) subLine diff --git a/src/fsharp/Ledger/ReportChartOfAccounts.fs b/src/fsharp/Ledger/ReportChartOfAccounts.fs index 36fcda1..a8e203a 100644 --- a/src/fsharp/Ledger/ReportChartOfAccounts.fs +++ b/src/fsharp/Ledger/ReportChartOfAccounts.fs @@ -28,12 +28,13 @@ let addLine (accounts: Accounts) (name: InputNameAccount) linesSoFar = let generateReport (input: InputFile) = let accounts = (Accounts (transactions input)) - let addLine = addLine accounts //partial application, one of the greatest features in F#, allows for cleaner code when we're simply passing a single, static variable - {Lines=([for account in accounts.Accounts -> - account.Key.AsInputName] - |> List.fold (fun (acc:Line list) (elem:InputNameAccount) -> - addLine elem acc) [])} - + let addLine = addLine accounts //partial application, one of the greatest features in F#, allows for cleaner code when we're simply passing multiple static variables + let lines = [for account in accounts.Accounts -> account.Key.AsInputName] + |> List.sortBy (fun x -> x.Entity) + |> List.fold (fun (acc:Line list) (elem:InputNameAccount) -> + addLine elem acc) [] + {Lines=lines} + let rec printLine indent (line : Line) = for i in 1 .. indent do printf " " @@ -52,8 +53,8 @@ let printReport report = |> List.map (fun l -> (l.Account.Entity, l)) |> List.collapse - for (key,lines) in collapsed do - printfn "%s/" key.AsString + for (entity,lines) in collapsed do + printfn "%s/" entity.AsString for line in lines do printLine 1 line printfn "" \ No newline at end of file From dd734e5adbc6b5c5c98ab28a106a01b68bc05365 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 21 Oct 2017 20:59:14 -0700 Subject: [PATCH 26/28] added entity support --- src/fsharp/Ledger/ReportBalanceSheet.fs | 15 +++++++++------ src/fsharp/Ledger/ReportProfitAndLoss.fs | 15 +++++++++++---- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/fsharp/Ledger/ReportBalanceSheet.fs b/src/fsharp/Ledger/ReportBalanceSheet.fs index b5988b5..ee79096 100644 --- a/src/fsharp/Ledger/ReportBalanceSheet.fs +++ b/src/fsharp/Ledger/ReportBalanceSheet.fs @@ -56,7 +56,7 @@ let rec constructReportBalanceSheetLine (accounts : Account option List) (accoun | Some account -> account.Balance | None -> zeroAmount) accounts) - { Account = (toInputName accountTree.Name) + { Account = InputName (accountTree.Name.[0].Input.Entity, Text.fmt accountTree.Name) //XXX: Ugh, I can't believe I keep doing this. There has to be a better way....... Amounts = { Balances = balances } SubAccounts = @@ -64,7 +64,7 @@ let rec constructReportBalanceSheetLine (accounts : Account option List) (accoun (constructReportBalanceSheetLine [ for a in accounts -> (extractSubAccount a child.Name) ] child) ] Postings = accountTree.Postings } -let addLine (name: InputNameAccount) (accounts: DatedAccounts) (dates: Date list) (linesSoFar : List) = +let addLine (accounts: DatedAccounts) (dates: Date list) (name: InputNameAccount) linesSoFar = let lastDate = (List.max dates) let finalAccounts = accounts.[lastDate] in match finalAccounts.find(name) with @@ -78,10 +78,13 @@ let addLine (name: InputNameAccount) (accounts: DatedAccounts) (dates: Date list let generateReport (input: InputFile) (dates: Date list) = let datedAccounts = (accountsByDate input dates) - { Dates = dates; - Lines = (addLine (InputName(Default, "Assets")) datedAccounts dates - (addLine (InputName(Default, "Liabilities")) datedAccounts dates - (addLine (InputName(Default, "Equity")) datedAccounts dates [])))} + let addLine = addLine datedAccounts dates + let lines = [for account in datedAccounts.[dates|>List.max].Accounts -> account.Value.FullInputName] + |> List.sortBy (fun x -> x.Entity.AsString) + |> List.fold (fun (acc:Line list) (elem:InputNameAccount) -> + addLine(elem) acc) [] + {Dates = dates; + Lines = lines} let rec printReportLine indent (line : Line) = for balance in line.Amounts.Balances do diff --git a/src/fsharp/Ledger/ReportProfitAndLoss.fs b/src/fsharp/Ledger/ReportProfitAndLoss.fs index 6dbf871..fb15318 100644 --- a/src/fsharp/Ledger/ReportProfitAndLoss.fs +++ b/src/fsharp/Ledger/ReportProfitAndLoss.fs @@ -61,7 +61,7 @@ let rec constructReportProfitAndLossLine (accounts : Account option List) (accou | Some account -> account.Balance | None -> zeroAmount) accounts) - { Account = accountTree.Name.[0].Input //XXX: Ugh, I can't believe I keep doing this. There has to be a better way....... + { Account = InputName (accountTree.Name.[0].Input.Entity, Text.fmt accountTree.Name) //XXX: Ugh, I can't believe I keep doing this. There has to be a better way....... Amounts = { Differences = (differences balances) } SubAccounts = @@ -69,7 +69,7 @@ let rec constructReportProfitAndLossLine (accounts : Account option List) (accou (constructReportProfitAndLossLine [ for a in accounts -> (extractSubAccount a child.Name) ] child) ] Postings = accountTree.Postings } -let addLine (name: InputNameAccount) (accounts: DatedAccounts) (dates: Date list) linesSoFar = +let addLine (accounts: DatedAccounts) (dates: Date list) (name: InputNameAccount) linesSoFar = let lastDate = (List.max dates) let finalAccounts = accounts.[lastDate] in match finalAccounts.find(name) with @@ -80,9 +80,16 @@ let addLine (name: InputNameAccount) (accounts: DatedAccounts) (dates: Date list let generateReport (input: InputFile) (dates: Date list) = let datedAccounts = (accountsByDate input dates) + let addLine = addLine datedAccounts dates + let lines = [for account in datedAccounts.[dates|>List.max].Accounts -> account.Value.FullInputName] + |> List.filter (fun x -> + let accountType = accountType x + (accountType = Income || accountType = Expense)) + |> List.sortBy (fun x -> x.Entity.AsString) + |> List.fold (fun (acc:Line list) (elem:InputNameAccount) -> + addLine(elem) acc) [] {Dates = dates; - Lines = (addLine (InputName(TEMPENTITY, "Income")) datedAccounts dates - (addLine (InputName(TEMPENTITY, "Expenses")) datedAccounts dates []))} + Lines = lines} let rec printReportLine indent (line : Line) = for difference in line.Amounts.Differences do From 6f88adeb46472adbc664d7aadaa670c09e9baa55 Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 21 Oct 2017 20:59:47 -0700 Subject: [PATCH 27/28] Text.fmt no longer includes entity by default, as it should be up to the caller to provide context --- src/fsharp/Ledger/TextOutput.fs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fsharp/Ledger/TextOutput.fs b/src/fsharp/Ledger/TextOutput.fs index 323b288..b7666da 100644 --- a/src/fsharp/Ledger/TextOutput.fs +++ b/src/fsharp/Ledger/TextOutput.fs @@ -10,7 +10,6 @@ open InternalTypes type Text = static member fmt (x:InternalNameAccount) = - let entity = x.[0].Input.Entity.AsString //XXX: There has to be a better way, perhaps InternalNameAccount will have Entity as a top level field? let rec helper (y:InternalNameAccount) = match y with | last::[] -> @@ -20,7 +19,7 @@ type Text = match first.Input with (InputName (entity, name)) -> (sprintf "%s:%s" name (helper rest)) | [] -> raise EmptyAccountNameComponentsException - (sprintf "%s/" entity) + (helper x) + (helper x) static member fmt (x: Amount) = match x with From e747ad2d17be76dc98f45be490eb8641b4b6b7ee Mon Sep 17 00:00:00 2001 From: FatherFoxxy Date: Sat, 21 Oct 2017 21:11:16 -0700 Subject: [PATCH 28/28] updated to work with entities --- src/fsharp/Ledger/ExcelOutput.fs | 118 ++++++++++++++----------------- 1 file changed, 54 insertions(+), 64 deletions(-) diff --git a/src/fsharp/Ledger/ExcelOutput.fs b/src/fsharp/Ledger/ExcelOutput.fs index 6f0be13..5241204 100644 --- a/src/fsharp/Ledger/ExcelOutput.fs +++ b/src/fsharp/Ledger/ExcelOutput.fs @@ -4,6 +4,7 @@ open InputTypes open TextOutput open InternalTypes open OfficeOpenXml +open Misc type Destination = ExcelPackage option @@ -403,10 +404,7 @@ type Excel = for c in (txnNumCol-1)..(txnNumCol+2) do worksheet.Column(c).AutoFit(0.0) - static member writeLine((line: ReportBalances.Line), - (ws : ExcelWorksheet), - (indent: int), - (nextRow: int)) = + static member writeLine((line: ReportBalances.Line), (ws : ExcelWorksheet), (indent: int), (nextRow: int)) = (Excel.setValue (ws.Cells.[nextRow, 1], line.Balance)) Excel.setValue (ws.Cells.[nextRow, 2+indent], line.Account.AsString) if indent <> 0 then @@ -416,10 +414,7 @@ type Excel = ws.Row(nextRow).Collapsed <- true rowAfterChildren - static member writeLines((lines : ReportBalances.Line list), - (ws : ExcelWorksheet), - (indent: int), - (nextRow: int)) = + static member writeLines((lines : ReportBalances.Line list), (ws : ExcelWorksheet), (indent: int), (nextRow: int)) = match lines with | [] -> nextRow | first::rest -> Excel.writeLines(rest, ws, indent, Excel.writeLine(first, ws, indent, nextRow)) @@ -435,55 +430,52 @@ type Excel = worksheet.View.FreezePanes(2, 1) worksheet.OutLineSummaryBelow <- false - static member writeLine((line: ReportChartOfAccounts.Line), - (ws : ExcelWorksheet), - (indent: int), - (nextRow: int)) = - Excel.setValue (ws.Cells.[nextRow, 1+indent], line.Account.AsString) - if indent <> 0 then + static member writeLine((line: ReportChartOfAccounts.Line), (ws : ExcelWorksheet), (indent: int), (nextRow: int)) = + Excel.setValue (ws.Cells.[nextRow, 1+indent], line.Account.Name) + if indent <> 1 then ws.Row(nextRow).OutlineLevel <- (indent) // Deliberately avoid collapsing hierarchy. If we're looking, we probably want to // emphasise details, and it's easy to manually hide them if that's what is wanted. Excel.writeLines (line.SubAccounts, ws, indent+1, nextRow+1) - static member writeLines((lines : ReportChartOfAccounts.Line list), - (ws : ExcelWorksheet), - (indent: int), - (nextRow: int)) = - match lines with - | [] -> nextRow - | first::rest -> Excel.writeLines(rest, ws, indent, Excel.writeLine(first, ws, indent, nextRow)) + static member writeLines((lines : ReportChartOfAccounts.Line list), (ws : ExcelWorksheet), (indent: int), (nextRow: int)) = + match lines with + | [] -> nextRow + | first::rest -> Excel.writeLines(rest, ws, indent, Excel.writeLine(first, ws, indent, nextRow)) static member write((report : ReportChartOfAccounts.Report), (destination : Destination)) = - match destination with - | None -> () - | Some package -> - let worksheet = package.Workbook.Worksheets.Add("Chart Of Accounts") - (setHeader worksheet.Cells.[1, 1] "Account") - Excel.writeLines(report.Lines, worksheet, 0, 2) |> ignore - worksheet.View.FreezePanes(2, 1) - worksheet.OutLineSummaryBelow <- false - - static member writeLine((line: ReportTransactionList.Line), - (ws : ExcelWorksheet), - (nextRow: int)) = - + match destination with + | None -> () + | Some package -> + let worksheet = package.Workbook.Worksheets.Add("Chart Of Accounts") + (setHeader worksheet.Cells.[1, 1] "Account") + report.Lines + |> List.map (fun line -> (line.Account.Entity, line)) + |> List.collapse + |> List.fold (fun nextRow (entity,accounts) -> + Excel.setValue(worksheet.Cells.[nextRow,1], entity.AsString) + Excel.writeLines(accounts, worksheet, 1, nextRow+1)) 2 + |> ignore + worksheet.View.FreezePanes(2, 1) + worksheet.OutLineSummaryBelow <- false + + static member writeLine((line: ReportTransactionList.Line), (ws : ExcelWorksheet), (nextRow: int)) = let txnCell = ws.Cells.[nextRow, 1] let dateCell = ws.Cells.[nextRow, 2] let descCell = ws.Cells.[nextRow, 3] - + Excel.setValue (txnCell, (sprintf "txn:%d" line.transaction.id)) txnCell.Style.Border.Top.Style <- OfficeOpenXml.Style.ExcelBorderStyle.Thin - + Excel.setValue (dateCell, line.transaction.date) dateCell.Style.Font.Bold <- true dateCell.Style.Border.Top.Style <- OfficeOpenXml.Style.ExcelBorderStyle.Thin - + Excel.setValue (descCell, line.transaction.description) descCell.Style.Font.Bold <- true descCell.Style.Font.Italic <- true descCell.Style.Border.Top.Style <- OfficeOpenXml.Style.ExcelBorderStyle.Thin - + line.transaction.postings |> List.fold (fun row p -> Excel.setValue (ws.Cells.[row, 2], p.amount) @@ -491,31 +483,29 @@ type Excel = row+1) (nextRow+1) - static member writeLines((lines : ReportTransactionList.Line list), - (ws : ExcelWorksheet), - (nextRow: int)) = - match lines with - | [] -> nextRow - | first::rest -> Excel.writeLines(rest, ws, Excel.writeLine(first, ws, nextRow)) + static member writeLines((lines : ReportTransactionList.Line list), (ws : ExcelWorksheet), (nextRow: int)) = + match lines with + | [] -> nextRow + | first::rest -> Excel.writeLines(rest, ws, Excel.writeLine(first, ws, nextRow)) static member write((report : ReportTransactionList.Report), (destination : Destination)) = - match destination with - | None -> () - | Some package -> - let worksheet = package.Workbook.Worksheets.Add("Transactions") - - match report.first with - | Some date -> (setHeader worksheet.Cells.[1, 1] "From:") - (setHeader worksheet.Cells.[1, 2] date) - | None -> () - match report.last with - | Some date -> (setHeader worksheet.Cells.[2, 1] "To:") - (setHeader worksheet.Cells.[2, 2] date) - | None -> () - (setHeader worksheet.Cells.[3, 1] "Transaction#") - (setHeader worksheet.Cells.[3, 2] "Date/Amount") - (setHeader worksheet.Cells.[3, 3] "Description/Account") - worksheet.View.FreezePanes(4, 1) - Excel.writeLines(report.lines, worksheet, 4) |> ignore - for c in 1..3 do - worksheet.Column(c).AutoFit(0.0) + match destination with + | None -> () + | Some package -> + let worksheet = package.Workbook.Worksheets.Add("Transactions") + + match report.first with + | Some date -> (setHeader worksheet.Cells.[1, 1] "From:") + (setHeader worksheet.Cells.[1, 2] date) + | None -> () + match report.last with + | Some date -> (setHeader worksheet.Cells.[2, 1] "To:") + (setHeader worksheet.Cells.[2, 2] date) + | None -> () + (setHeader worksheet.Cells.[3, 1] "Transaction#") + (setHeader worksheet.Cells.[3, 2] "Date/Amount") + (setHeader worksheet.Cells.[3, 3] "Description/Account") + worksheet.View.FreezePanes(4, 1) + Excel.writeLines(report.lines, worksheet, 4) |> ignore + for c in 1..3 do + worksheet.Column(c).AutoFit(0.0)