{-| A ledger-compatible @balance@ command. ledger's balance command is easy to use but not easy to describe precisely. In the examples below we'll use sample.journal, which has the following account tree: @ assets bank checking saving cash expenses food supplies income gifts salary liabilities debts @ The balance command shows accounts with their aggregate balances. Subaccounts are displayed indented below their parent. Each balance is the sum of any transactions in that account plus any balances from subaccounts: @ $ hledger -f sample.journal balance $-1 assets $1 bank:saving $-2 cash $2 expenses $1 food $1 supplies $-2 income $-1 gifts $-1 salary $1 liabilities:debts @ Usually, the non-interesting accounts are elided or omitted. Above, @checking@ is omitted because it has no subaccounts and a zero balance. @bank@ is elided because it has only a single displayed subaccount (@saving@) and it would be showing the same balance as that ($1). Ditto for @liabilities@. We will return to this in a moment. The --depth argument can be used to limit the depth of the balance report. So, to see just the top level accounts: @ $ hledger -f sample.journal balance --depth 1 $-1 assets $2 expenses $-2 income $1 liabilities @ This time liabilities has no displayed subaccounts (due to --depth) and is not elided. With one or more account pattern arguments, the balance command shows accounts whose name matches one of the patterns, plus their parents (elided) and subaccounts. So with the pattern o we get: @ $ hledger -f sample.journal balance o $1 expenses:food $-2 income $-1 gifts $-1 salary -------------------- $-1 @ The o pattern matched @food@ and @income@, so they are shown. Unmatched parents of matched accounts are also shown (elided) for context (@expenses@). Also, the balance report shows the total of all displayed accounts, when that is non-zero. Here, it is displayed because the accounts shown add up to $-1. Here is a more precise definition of \"interesting\" accounts in ledger's balance report: - an account which has just one interesting subaccount branch, and which is not at the report's maximum depth, is interesting if the balance is different from the subaccount's, and otherwise boring. - any other account is interesting if it has a non-zero balance, or the -E flag is used. -} module Hledger.Cli.Balance ( balance ,accountsReportAsText ,tests_Hledger_Cli_Balance ) where import Data.List import Data.Maybe import Test.HUnit import Hledger import Prelude hiding (putStr) import Hledger.Utils.UTF8 (putStr) import Hledger.Cli.Format import qualified Hledger.Cli.Format as Format import Hledger.Cli.Options -- | Print a balance report. balance :: CliOpts -> Journal -> IO () balance CliOpts{reportopts_=ropts} j = do d <- getCurrentDay let lines = case formatFromOpts ropts of Left err -> [err] Right _ -> accountsReportAsText ropts $ accountsReport ropts (optsToFilterSpec ropts d) j putStr $ unlines lines -- | Render a balance report as plain text suitable for console output. accountsReportAsText :: ReportOpts -> AccountsReport -> [String] accountsReportAsText opts (items, total) = concat lines ++ t where lines = case formatFromOpts opts of Right f -> map (accountsReportItemAsText opts f) items Left err -> [[err]] t = if no_total_ opts then [] else ["--------------------" -- TODO: This must use the format somehow ,padleft 20 $ showMixedAmountWithoutPrice total ] {- This implementation turned out to be a bit convoluted but implements the following algorithm for formatting: - If there is a single amount, print it with the account name directly: - Otherwise, only print the account name on the last line. a USD 1 ; Account 'a' has a single amount EUR -1 b USD -1 ; Account 'b' has two amounts. The account name is printed on the last line. -} -- | Render one balance report line item as plain text. accountsReportItemAsText :: ReportOpts -> [FormatString] -> AccountsReportItem -> [String] accountsReportItemAsText opts format (_, accountName, depth, Mixed amounts) = case amounts of [] -> [] [a] -> [formatAccountsReportItem opts (Just accountName) depth a format] (as) -> multiline as where multiline :: [Amount] -> [String] multiline [] = [] multiline [a] = [formatAccountsReportItem opts (Just accountName) depth a format] multiline (a:as) = (formatAccountsReportItem opts Nothing depth a format) : multiline as formatAccountsReportItem :: ReportOpts -> Maybe AccountName -> Int -> Amount -> [FormatString] -> String formatAccountsReportItem _ _ _ _ [] = "" formatAccountsReportItem opts accountName depth amount (fmt:fmts) = s ++ (formatAccountsReportItem opts accountName depth amount fmts) where s = case fmt of FormatLiteral l -> l FormatField ljust min max field -> formatField opts accountName depth amount ljust min max field formatField :: ReportOpts -> Maybe AccountName -> Int -> Amount -> Bool -> Maybe Int -> Maybe Int -> Field -> String formatField opts accountName depth total ljust min max field = case field of Format.Account -> formatValue ljust min max $ maybe "" (accountNameDrop (drop_ opts)) accountName Format.DepthSpacer -> case min of Just m -> formatValue ljust Nothing max $ replicate (depth * m) ' ' Nothing -> formatValue ljust Nothing max $ replicate depth ' ' Format.Total -> formatValue ljust min max $ showAmountWithoutPrice total _ -> "" tests_Hledger_Cli_Balance = TestList [ ]