diff --git a/hledger-lib/Hledger/Data/Account.hs b/hledger-lib/Hledger/Data/Account.hs index f92d3cbdf..f7d13c5d3 100644 --- a/hledger-lib/Hledger/Data/Account.hs +++ b/hledger-lib/Hledger/Data/Account.hs @@ -46,7 +46,7 @@ instance Eq Account where nullacct = Account { aname = "" - , acode = Nothing + , adeclarationorder = Nothing , aparent = Nothing , asubs = [] , anumpostings = 0 @@ -67,9 +67,8 @@ accountsFromPostings ps = grouped = groupSort [(paccount p,pamount p) | p <- ps] counted = [(aname, length amts) | (aname, amts) <- grouped] summed = [(aname, sumStrict amts) | (aname, amts) <- grouped] -- always non-empty - nametree = treeFromPaths $ map (expandAccountName . fst) summed - acctswithnames = nameTreeToAccount "root" nametree - acctswithnumps = mapAccounts setnumps acctswithnames where setnumps a = a{anumpostings=fromMaybe 0 $ lookup (aname a) counted} + acctstree = accountTree "root" $ map fst summed + acctswithnumps = mapAccounts setnumps acctstree where setnumps a = a{anumpostings=fromMaybe 0 $ lookup (aname a) counted} acctswithebals = mapAccounts setebalance acctswithnumps where setebalance a = a{aebalance=lookupJustDef nullmixedamt (aname a) summed} acctswithibals = sumAccounts acctswithebals acctswithparents = tieAccountParents acctswithibals @@ -77,10 +76,14 @@ accountsFromPostings ps = in acctsflattened --- | Convert an AccountName tree to an Account tree -nameTreeToAccount :: AccountName -> FastTree AccountName -> Account -nameTreeToAccount rootname (T m) = - nullacct{ aname=rootname, asubs=map (uncurry nameTreeToAccount) $ M.assocs m } +-- | Convert a list of account names to a tree of Account objects, +-- with just the account names filled in. +-- A single root account with the given name is added. +accountTree :: AccountName -> [AccountName] -> Account +accountTree rootname as = nullacct{aname=rootname, asubs=map (uncurry accountTree') $ M.assocs m } + where + T m = treeFromPaths $ map expandAccountName as :: FastTree AccountName + accountTree' a (T m) = nullacct{aname=a, asubs=map (uncurry accountTree') $ M.assocs m} -- | Tie the knot so all subaccounts' parents are set correctly. tieAccountParents :: Account -> Account @@ -90,10 +93,6 @@ tieAccountParents = tie Nothing where a' = a{aparent=parent, asubs=map (tie (Just a')) asubs} --- | Look up an account's numeric code, if any, from the Journal and set it. -accountSetCodeFrom :: Journal -> Account -> Account -accountSetCodeFrom j a = a{acode=fromMaybe Nothing $ lookup (aname a) (jdeclaredaccounts j)} - -- | Get this account's parent accounts, from the nearest up to the root. parentAccounts :: Account -> [Account] parentAccounts Account{aparent=Nothing} = [] @@ -189,7 +188,7 @@ filterAccounts p a | p a = a : concatMap (filterAccounts p) (asubs a) | otherwise = concatMap (filterAccounts p) (asubs a) --- | Sort each level of an account tree by inclusive amount, +-- | Sort each group of siblings in an account tree by inclusive amount, -- so that the accounts with largest normal balances are listed first. -- The provided normal balance sign determines whether normal balances -- are negative or positive, affecting the sort order. Ie, @@ -199,24 +198,54 @@ sortAccountTreeByAmount :: NormalSign -> Account -> Account sortAccountTreeByAmount normalsign a | null $ asubs a = a | otherwise = a{asubs= - sortBy (maybeflip $ comparing aibalance) $ + sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . aibalance)) $ map (sortAccountTreeByAmount normalsign) $ asubs a} where maybeflip | normalsign==NormallyNegative = id | otherwise = flip --- | Sort each level of an account tree first by the account code --- if any, with the empty account code sorting last, and then by --- the account name. -sortAccountTreeByAccountCodeAndName :: Account -> Account -sortAccountTreeByAccountCodeAndName a +-- | Look up an account's declaration order, if any, from the Journal and set it. +-- This is the relative position of its account directive +-- among the other account directives. +accountSetDeclarationOrder :: Journal -> Account -> Account +accountSetDeclarationOrder j a@Account{..} = + a{adeclarationorder = findIndex (==aname) (jdeclaredaccounts j)} + +-- | Sort account names by the order in which they were declared in +-- the journal, at each level of the account tree (ie within each +-- group of siblings). Undeclared accounts are sorted last and +-- alphabetically. +-- This is hledger's default sort for reports organised by account. +-- The account list is converted to a tree temporarily, adding any +-- missing parents; these can be kept (suitable for a tree-mode report) +-- or removed (suitable for a flat-mode report). +-- +sortAccountNamesByDeclaration :: Journal -> Bool -> [AccountName] -> [AccountName] +sortAccountNamesByDeclaration j keepparents as = + (if keepparents then id else filter (`elem` as)) $ -- maybe discard missing parents that were added + map aname $ -- keep just the names + drop 1 $ -- drop the root node that was added + flattenAccounts $ -- convert to an account list + sortAccountTreeByDeclaration $ -- sort by declaration order (and name) + mapAccounts (accountSetDeclarationOrder j) $ -- add declaration order info + accountTree "root" -- convert to an account tree + as + +-- | Sort each group of siblings in an account tree by declaration order, then account name. +-- So each group will contain first the declared accounts, +-- in the same order as their account directives were parsed, +-- and then the undeclared accounts, sorted by account name. +sortAccountTreeByDeclaration :: Account -> Account +sortAccountTreeByDeclaration a | null $ asubs a = a | otherwise = a{asubs= - sortBy (comparing accountCodeAndNameForSort) $ map sortAccountTreeByAccountCodeAndName $ asubs a} + sortBy (comparing accountDeclarationOrderAndName) $ + map sortAccountTreeByDeclaration $ asubs a + } -accountCodeAndNameForSort a = (acode', aname a) +accountDeclarationOrderAndName a = (adeclarationorder', aname a) where - acode' = fromMaybe maxBound (acode a) + adeclarationorder' = fromMaybe maxBound (adeclarationorder a) -- | Search an account list by name. lookupAccount :: AccountName -> [Account] -> Maybe Account diff --git a/hledger-lib/Hledger/Data/Journal.hs b/hledger-lib/Hledger/Data/Journal.hs index 65977094f..e7ae1860a 100644 --- a/hledger-lib/Hledger/Data/Journal.hs +++ b/hledger-lib/Hledger/Data/Journal.hs @@ -256,7 +256,7 @@ journalAccountNamesImplied = expandAccountNames . journalAccountNamesUsed -- | Sorted unique account names declared by account directives in this journal. journalAccountNamesDeclared :: Journal -> [AccountName] -journalAccountNamesDeclared = nub . sort . map fst . jdeclaredaccounts +journalAccountNamesDeclared = nub . sort . jdeclaredaccounts -- | Sorted unique account names declared by account directives or posted to -- by transactions in this journal. @@ -493,7 +493,8 @@ journalFinalise t path txt assrt j@Journal{jfiles=fs} = journalApplyCommodityStyles $ j {jfiles = (path,txt) : reverse fs ,jlastreadtime = t - ,jtxns = reverse $ jtxns j -- NOTE: see addTransaction + ,jdeclaredaccounts = reverse $ jdeclaredaccounts j + ,jtxns = reverse $ jtxns j -- NOTE: see addTransaction ,jtxnmodifiers = reverse $ jtxnmodifiers j -- NOTE: see addTransactionModifier ,jperiodictxns = reverse $ jperiodictxns j -- NOTE: see addPeriodicTransaction ,jmarketprices = reverse $ jmarketprices j -- NOTE: see addMarketPrice diff --git a/hledger-lib/Hledger/Data/Ledger.hs b/hledger-lib/Hledger/Data/Ledger.hs index 49ac38c63..b6d963a77 100644 --- a/hledger-lib/Hledger/Data/Ledger.hs +++ b/hledger-lib/Hledger/Data/Ledger.hs @@ -63,7 +63,7 @@ ledgerFromJournal q j = nullledger{ljournal=j'', laccounts=as} (q',depthq) = (filterQuery (not . queryIsDepth) q, filterQuery queryIsDepth q) j' = filterJournalAmounts (filterQuery queryIsSym q) $ -- remove amount parts which the query's sym: terms would exclude filterJournalPostings q' j - as = map (accountSetCodeFrom j) $ accountsFromPostings $ journalPostings j' + as = accountsFromPostings $ journalPostings j' j'' = filterJournalPostings depthq j' -- | List a ledger's account names. diff --git a/hledger-lib/Hledger/Data/Types.hs b/hledger-lib/Hledger/Data/Types.hs index 8d438befe..da44b18be 100644 --- a/hledger-lib/Hledger/Data/Types.hs +++ b/hledger-lib/Hledger/Data/Types.hs @@ -358,11 +358,11 @@ data Journal = Journal { ,jparseparentaccounts :: [AccountName] -- ^ the current stack of parent account names, specified by apply account directives ,jparsealiases :: [AccountAlias] -- ^ the current account name aliases in effect, specified by alias directives (& options ?) -- ,jparsetransactioncount :: Integer -- ^ the current count of transactions parsed so far (only journal format txns, currently) - ,jparsetimeclockentries :: [TimeclockEntry] -- ^ timeclock sessions which have not been clocked out + ,jparsetimeclockentries :: [TimeclockEntry] -- ^ timeclock sessions which have not been clocked out -- principal data - ,jdeclaredaccounts :: [(AccountName, Maybe AccountCode)] -- ^ Accounts declared by account directives, in parse order. + ,jdeclaredaccounts :: [AccountName] -- ^ Accounts declared by account directives, in parse order (after journal finalisation) ,jcommodities :: M.Map CommoditySymbol Commodity -- ^ commodities and formats declared by commodity directives - ,jinferredcommodities :: M.Map CommoditySymbol AmountStyle -- ^ commodities and formats inferred from journal amounts XXX misnamed + ,jinferredcommodities :: M.Map CommoditySymbol AmountStyle -- ^ commodities and formats inferred from journal amounts TODO misnamed - jusedstyles ,jmarketprices :: [MarketPrice] ,jtxnmodifiers :: [TransactionModifier] ,jperiodictxns :: [PeriodicTransaction] @@ -392,7 +392,7 @@ type StorageFormat = String -- which let you walk up or down the account tree. data Account = Account { aname :: AccountName, -- ^ this account's full name - acode :: Maybe AccountCode, -- ^ this account's numeric code, if any (not always set) + adeclarationorder :: Maybe Int , -- ^ the relative position of this account's account directive, if any. Normally a natural number. aebalance :: MixedAmount, -- ^ this account's balance, excluding subaccounts asubs :: [Account], -- ^ sub-accounts anumpostings :: Int, -- ^ number of postings to this account diff --git a/hledger-lib/Hledger/Read/JournalReader.hs b/hledger-lib/Hledger/Read/JournalReader.hs index 50a7e32c3..c4024ef15 100644 --- a/hledger-lib/Hledger/Read/JournalReader.hs +++ b/hledger-lib/Hledger/Read/JournalReader.hs @@ -261,8 +261,7 @@ accountdirectivep = do string "account" lift (skipSome spacenonewline) acct <- modifiedaccountnamep -- account directives can be modified by alias/apply account - macode' :: Maybe String <- (optional $ lift $ skipSome spacenonewline >> some digitChar) - let macode :: Maybe AccountCode = read <$> macode' + _ :: Maybe String <- (optional $ lift $ skipSome spacenonewline >> some digitChar) -- compatibility: ignore account codes supported in 1.9/1.10 newline skipMany indentedlinep pushDeclaredAccount acct diff --git a/hledger-lib/Hledger/Reports/BalanceReport.hs b/hledger-lib/Hledger/Reports/BalanceReport.hs index d50fc491f..67c0ef7a4 100644 --- a/hledger-lib/Hledger/Reports/BalanceReport.hs +++ b/hledger-lib/Hledger/Reports/BalanceReport.hs @@ -18,6 +18,7 @@ module Hledger.Reports.BalanceReport ( BalanceReportItem, balanceReport, flatShowsExclusiveBalance, + sortAccountItemsLike, -- * Tests tests_BalanceReport @@ -78,7 +79,7 @@ flatShowsExclusiveBalance = True balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport balanceReport opts q j = (if invert_ opts then brNegate else id) $ - (items, total) + (sorteditems, total) where -- dbg1 = const id -- exclude from debug output dbg1 s = let p = "balanceReport" in Hledger.Utils.dbg1 (p++" "++s) -- add prefix in debug output @@ -89,7 +90,6 @@ balanceReport opts q j = dbg1 "accts" $ take 1 $ clipAccountsAndAggregate (queryDepth q) $ flattenAccounts accts | flat_ opts = dbg1 "accts" $ - sortflat $ filterzeros $ filterempty $ drop 1 $ clipAccountsAndAggregate (queryDepth q) $ flattenAccounts accts @@ -98,27 +98,52 @@ balanceReport opts q j = drop 1 $ flattenAccounts $ markboring $ prunezeros $ - sorttree $ + sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ opts) $ clipAccounts (queryDepth q) accts where - balance = if flat_ opts then aebalance else aibalance + balance = if flat_ opts then aebalance else aibalance filterzeros = if empty_ opts then id else filter (not . isZeroMixedAmount . balance) filterempty = filter (\a -> anumpostings a > 0 || not (isZeroMixedAmount (balance a))) prunezeros = if empty_ opts then id else fromMaybe nullacct . pruneAccounts (isZeroMixedAmount . balance) markboring = if no_elide_ opts then id else markBoringParentAccounts - sortflat | sort_amount_ opts = sortBy (maybeflip $ comparing balance) - | otherwise = sortBy (comparing accountCodeAndNameForSort) - where - maybeflip = if normalbalance_ opts == Just NormallyNegative then id else flip - sorttree | sort_amount_ opts = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ opts) - | otherwise = sortAccountTreeByAccountCodeAndName + items = dbg1 "items" $ map (balanceReportItem opts q) accts' + + -- now sort items like MultiBalanceReport, except + -- sorting a tree by amount was more easily done above + sorteditems + | sort_amount_ opts && tree_ opts = items + | sort_amount_ opts = sortFlatBRByAmount items + | otherwise = sortBRByAccountDeclaration items + + where + -- Sort the report rows, representing a flat account list, by row total. + sortFlatBRByAmount :: [BalanceReportItem] -> [BalanceReportItem] + sortFlatBRByAmount = sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . fourth4)) + where + maybeflip = if normalbalance_ opts == Just NormallyNegative then id else flip + + -- Sort the report rows by account declaration order then account name. + sortBRByAccountDeclaration :: [BalanceReportItem] -> [BalanceReportItem] + sortBRByAccountDeclaration rows = sortedrows + where + anamesandrows = [(first4 r, r) | r <- rows] + anames = map fst anamesandrows + sortedanames = sortAccountNamesByDeclaration j (tree_ opts) anames + sortedrows = sortAccountItemsLike sortedanames anamesandrows + total | not (flat_ opts) = dbg1 "total" $ sum [amt | (_,_,indent,amt) <- items, indent == 0] | otherwise = dbg1 "total" $ if flatShowsExclusiveBalance then sum $ map fourth4 items else sum $ map aebalance $ clipAccountsAndAggregate 1 accts' +-- | A sorting helper: sort a list of things (eg report rows) keyed by account name +-- to match the provided ordering of those same account names. +sortAccountItemsLike :: [AccountName] -> [(AccountName, b)] -> [b] +sortAccountItemsLike sortedas items = + concatMap (\a -> maybe [] (:[]) $ lookup a items) sortedas + -- | In an account tree with zero-balance leaves removed, mark the -- elidable parent accounts (those with one subaccount and no balance -- of their own). diff --git a/hledger-lib/Hledger/Reports/BudgetReport.hs b/hledger-lib/Hledger/Reports/BudgetReport.hs index c35f7e2bb..fc30a73b5 100644 --- a/hledger-lib/Hledger/Reports/BudgetReport.hs +++ b/hledger-lib/Hledger/Reports/BudgetReport.hs @@ -178,69 +178,46 @@ combineBudgetAndActual acctsdone = map first6 rows1 -- combine and re-sort rows - -- TODO: respect hierarchy in tree mode + -- TODO: use MBR code -- TODO: respect --sort-amount -- TODO: add --sort-budget to sort by budget goal amount rows :: [PeriodicReportRow (Maybe Change, Maybe BudgetGoal)] = sortBy (comparing first6) $ rows1 ++ rows2 --- massive duplication from multiBalanceReport to handle tree mode sorting ? --- dbg1 "sorteditems" $ --- sortitems items + +-- -- like MultiBalanceReport +-- sortedrows +-- | sort_amount_ opts && tree_ opts = sortTreeBURByAmount items +-- | sort_amount_ opts = sortFlatBURByAmount items +-- | otherwise = sortBURByAccountDeclaration items +-- -- where --- sortitems --- | sort_amount_ opts && accountlistmode_ opts == ALTree = sortTreeMultiBalanceReportRowsByAmount --- | sort_amount_ opts = sortFlatMultiBalanceReportRowsByAmount --- | not (sort_amount_ opts) && accountlistmode_ opts == ALTree = sortTreeMultiBalanceReportRowsByAccountCodeAndName --- | otherwise = sortFlatMultiBalanceReportRowsByAccountCodeAndName +-- -- Sort the report rows, representing a tree of accounts, by row total at each level. +-- sortTreeMBRByAmount rows = sortedrows -- where --- -- Sort the report rows, representing a flat account list, by row total. --- sortFlatMultiBalanceReportRowsByAmount = sortBy (maybeflip $ comparing fifth6) +-- anamesandrows = [(first6 r, r) | r <- rows] +-- anames = map fst anamesandrows +-- atotals = [(a,tot) | (a,_,_,_,tot,_) <- rows] +-- accounttree = accountTree "root" anames +-- accounttreewithbals = mapAccounts setibalance accounttree -- where --- maybeflip = if normalbalance_ opts == Just NormallyNegative then id else flip +-- -- should not happen, but it's ugly; TODO +-- setibalance a = a{aibalance=fromMaybe (error "sortTreeBURByAmount 1") $ lookup (aname a) atotals} +-- sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ opts) accounttreewithbals +-- sortedanames = map aname $ drop 1 $ flattenAccounts sortedaccounttree +-- sortedrows = sortAccountItemsLike sortedanames anamesandrows -- --- -- Sort the report rows, representing a tree of accounts, by row total at each level. --- -- To do this we recreate an Account tree with the row totals as balances, --- -- so we can do a hierarchical sort, flatten again, and then reorder the --- -- report rows similarly. Yes this is pretty long winded. --- sortTreeMultiBalanceReportRowsByAmount rows = sortedrows --- where --- anamesandrows = [(first6 r, r) | r <- rows] --- anames = map fst anamesandrows --- atotals = [(a,tot) | (a,_,_,_,tot,_) <- rows] --- nametree = treeFromPaths $ map expandAccountName anames --- accounttree = nameTreeToAccount "root" nametree --- accounttreewithbals = mapAccounts setibalance accounttree --- where --- -- this error should not happen, but it's ugly TODO --- setibalance a = a{aibalance=fromMaybe (error "sortTreeMultiBalanceReportRowsByAmount 1") $ lookup (aname a) atotals} --- sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ opts) accounttreewithbals --- sortedaccounts = drop 1 $ flattenAccounts sortedaccounttree --- -- dropped the root account, also ignore any parent accounts not in rows --- sortedrows = concatMap (\a -> maybe [] (:[]) $ lookup (aname a) anamesandrows) sortedaccounts --- --- -- Sort the report rows by account code if any, with the empty account code coming last, then account name. --- sortFlatMultiBalanceReportRowsByAccountCodeAndName = sortBy (comparing acodeandname) --- where --- acodeandname r = (acode', aname) --- where --- aname = first6 r --- macode = fromMaybe Nothing $ lookup aname $ jdeclaredaccounts j --- acode' = fromMaybe maxBound macode --- --- -- Sort the report rows, representing a tree of accounts, by account code and then account name at each level. --- -- Convert a tree of account names, look up the account codes, sort and flatten the tree, reorder the rows. --- sortTreeMultiBalanceReportRowsByAccountCodeAndName rows = sortedrows --- where --- anamesandrows = [(first6 r, r) | r <- rows] --- anames = map fst anamesandrows --- nametree = treeFromPaths $ map expandAccountName anames --- accounttree = nameTreeToAccount "root" nametree --- accounttreewithcodes = mapAccounts (accountSetCodeFrom j) accounttree --- sortedaccounttree = sortAccountTreeByAccountCodeAndName accounttreewithcodes --- sortedaccounts = drop 1 $ flattenAccounts sortedaccounttree --- -- dropped the root account, also ignore any parent accounts not in rows --- sortedrows = concatMap (\a -> maybe [] (:[]) $ lookup (aname a) anamesandrows) sortedaccounts +-- -- Sort the report rows, representing a flat account list, by row total. +-- sortFlatBURByAmount = sortBy (maybeflip $ comparing fifth6) +-- where +-- maybeflip = if normalbalance_ opts == Just NormallyNegative then id else flip -- +-- -- Sort the report rows by account declaration order then account name. +-- sortBURByAccountDeclaration rows = sortedrows +-- where +-- anamesandrows = [(first6 r, r) | r <- rows] +-- anames = map fst anamesandrows +-- sortedanames = sortAccountNamesByDeclaration j (tree_ opts) anames +-- sortedrows = sortAccountItemsLike sortedanames anamesandrows -- TODO: grand total & average shows 0% when there are no actual amounts, inconsistent with other cells totalrow = diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReports.hs b/hledger-lib/Hledger/Reports/MultiBalanceReports.hs index 75a20e492..03fd5e1c8 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReports.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReports.hs @@ -175,64 +175,45 @@ multiBalanceReport opts q j = , empty_ opts || depth == 0 || any (not . isZeroMixedAmount) displayedBals ] + -- TODO TBD: is it always ok to sort report rows after report has been generated ? + -- Or does sorting sometimes need to be done as part of the report generation ? sorteditems :: [MultiBalanceReportRow] = dbg1 "sorteditems" $ sortitems items where sortitems - | sort_amount_ opts && accountlistmode_ opts == ALTree = sortTreeMultiBalanceReportRowsByAmount - | sort_amount_ opts = sortFlatMultiBalanceReportRowsByAmount - | not (sort_amount_ opts) && accountlistmode_ opts == ALTree = sortTreeMultiBalanceReportRowsByAccountCodeAndName - | otherwise = sortFlatMultiBalanceReportRowsByAccountCodeAndName + | sort_amount_ opts && accountlistmode_ opts == ALTree = sortTreeMBRByAmount + | sort_amount_ opts = sortFlatMBRByAmount + | otherwise = sortMBRByAccountDeclaration where - -- Sort the report rows, representing a flat account list, by row total. - sortFlatMultiBalanceReportRowsByAmount = sortBy (maybeflip $ comparing fifth6) - where - maybeflip = if normalbalance_ opts == Just NormallyNegative then id else flip - -- Sort the report rows, representing a tree of accounts, by row total at each level. - -- To do this we recreate an Account tree with the row totals as balances, - -- so we can do a hierarchical sort, flatten again, and then reorder the - -- report rows similarly. Yes this is pretty long winded. - sortTreeMultiBalanceReportRowsByAmount rows = sortedrows + -- Similar to sortMBRByAccountDeclaration/sortAccountNamesByDeclaration. + sortTreeMBRByAmount rows = sortedrows where anamesandrows = [(first6 r, r) | r <- rows] anames = map fst anamesandrows atotals = [(a,tot) | (a,_,_,_,tot,_) <- rows] - nametree = treeFromPaths $ map expandAccountName anames - accounttree = nameTreeToAccount "root" nametree + accounttree = accountTree "root" anames accounttreewithbals = mapAccounts setibalance accounttree where - -- this error should not happen, but it's ugly TODO - setibalance a = a{aibalance=fromMaybe (error "sortTreeMultiBalanceReportRowsByAmount 1") $ lookup (aname a) atotals} + -- should not happen, but it's dangerous; TODO + setibalance a = a{aibalance=fromMaybe (error "sortTreeMBRByAmount 1") $ lookup (aname a) atotals} sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ opts) accounttreewithbals - sortedaccounts = drop 1 $ flattenAccounts sortedaccounttree - -- dropped the root account, also ignore any parent accounts not in rows - sortedrows = concatMap (\a -> maybe [] (:[]) $ lookup (aname a) anamesandrows) sortedaccounts + sortedanames = map aname $ drop 1 $ flattenAccounts sortedaccounttree + sortedrows = sortAccountItemsLike sortedanames anamesandrows - -- Sort the report rows by account code if any, with the empty account code coming last, then account name. - -- TODO keep children below their parent. Have to convert to tree ? - sortFlatMultiBalanceReportRowsByAccountCodeAndName = sortBy (comparing acodeandname) + -- Sort the report rows, representing a flat account list, by row total. + sortFlatMBRByAmount = sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . fifth6)) where - acodeandname r = (acode', aname) - where - aname = first6 r - macode = fromMaybe Nothing $ lookup aname $ jdeclaredaccounts j - acode' = fromMaybe maxBound macode + maybeflip = if normalbalance_ opts == Just NormallyNegative then id else flip - -- Sort the report rows, representing a tree of accounts, by account code and then account name at each level. - -- Convert a tree of account names, look up the account codes, sort and flatten the tree, reorder the rows. - sortTreeMultiBalanceReportRowsByAccountCodeAndName rows = sortedrows - where + -- Sort the report rows by account declaration order then account name. + sortMBRByAccountDeclaration rows = sortedrows + where anamesandrows = [(first6 r, r) | r <- rows] anames = map fst anamesandrows - nametree = treeFromPaths $ map expandAccountName anames - accounttree = nameTreeToAccount "root" nametree - accounttreewithcodes = mapAccounts (accountSetCodeFrom j) accounttree - sortedaccounttree = sortAccountTreeByAccountCodeAndName accounttreewithcodes - sortedaccounts = drop 1 $ flattenAccounts sortedaccounttree - -- dropped the root account, also ignore any parent accounts not in rows - sortedrows = concatMap (\a -> maybe [] (:[]) $ lookup (aname a) anamesandrows) sortedaccounts + sortedanames = sortAccountNamesByDeclaration j (tree_ opts) anames + sortedrows = sortAccountItemsLike sortedanames anamesandrows totals :: [MixedAmount] = -- dbg1 "totals" $ diff --git a/hledger-lib/hledger_journal.m4.md b/hledger-lib/hledger_journal.m4.md index 25cc6cc9b..f4fab8112 100644 --- a/hledger-lib/hledger_journal.m4.md +++ b/hledger-lib/hledger_journal.m4.md @@ -820,35 +820,9 @@ Currently this mainly helps with account name autocompletion in eg hledger add, hledger-iadd, hledger-web, and ledger-mode. In future it will also help detect misspelled accounts. -Account names can be followed by a numeric account code: -```journal -account assets 1000 -account assets:bank:checking 1110 -account liabilities 2000 -account revenues 4000 -account expenses 6000 -``` -This affects how accounts are sorted in account and balance reports: -accounts with codes are listed before accounts without codes, and in increasing code order -(instead of listing all accounts alphabetically). -Warning, this feature is incomplete; account codes do not yet affect sort order in - -- the `accounts` command -- the `balance` command's single-column mode -- flat mode balance reports - (to work around this, declare account codes on the subaccounts as well). -- hledger-web's sidebar - -Account codes should be all numeric digits, unique, and separated from the account name by at least two spaces (since account names may contain single spaces). -By convention, often the first digit indicates the type of account, -as in -[this numbering scheme](http://www.dwmbeancounter.com/BCTutorSite/Courses/ChartAccounts/lesson02-6.html) -and the example above. -In future, we might use this to recognize account types. - An account directive can also have indented subdirectives following it, which are currently ignored. Here is the full syntax: ```journal -; account ACCTNAME [OPTIONALCODE] +; account ACCTNAME ; [OPTIONALSUBDIRECTIVES] account assets:bank:checking 1110 @@ -856,6 +830,54 @@ account assets:bank:checking 1110 some-tag:12345 ``` +### Account display order + +Account directives have another purpose: they set the display order of accounts in reports. +For example, say you have these top-level accounts: +```shell +$ accounts -1 +assets +equity +expenses +liabilities +misc +other +revenues +``` + +Ie without account declarations, they are displayed in alphabetical order. +But if you add the following account directives to the journal: +```journal +account assets +account liabilities +account equity +account revenues +account expenses +``` + +the display order changes to: +```shell +$ accounts -1 +assets +liabilities +equity +revenues +expenses +misc +other +``` + +Ie, declared accounts first, in declaration order, followed by undeclared accounts in alphabetic order. + +Warning: work in progress. +This is supported by +the accounts command +and by tabular balance reports (`balancesheet`, `balance -Y`, etc). +It is not yet supported by +non-tabular balance reports, +budget reports, +or hledger-web's sidebar. + ### Rewriting accounts You can define account alias rules which rewrite your account names, or parts of them, diff --git a/hledger/Hledger/Cli/Commands/Accounts.hs b/hledger/Hledger/Cli/Commands/Accounts.hs index 8e5e0cf0f..e5d5e989c 100644 --- a/hledger/Hledger/Cli/Commands/Accounts.hs +++ b/hledger/Hledger/Cli/Commands/Accounts.hs @@ -10,6 +10,7 @@ The @accounts@ command lists account names: -} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE CPP #-} @@ -19,17 +20,15 @@ module Hledger.Cli.Commands.Accounts ( ,accounts ) where -import Data.List #if !(MIN_VERSION_base(4,11,0)) import Data.Monoid #endif --- import Data.Text (Text) +import Data.List import qualified Data.Text as T +import qualified Data.Text.IO as T import System.Console.CmdArgs.Explicit as C import Hledger -import Prelude hiding (putStrLn) -import Hledger.Utils.UTF8IOCompat (putStrLn) import Hledger.Cli.CliOptions @@ -64,20 +63,39 @@ accountsmode = (defCommandMode $ ["accounts"] ++ aliases) { -- | The accounts command. accounts :: CliOpts -> Journal -> IO () accounts CliOpts{rawopts_=rawopts, reportopts_=ropts} j = do + + -- 1. identify the accounts we'll show d <- getCurrentDay - let q = queryFromOpts d ropts + let tree = tree_ ropts + declared = boolopt "declared" rawopts + used = boolopt "used" rawopts + q = queryFromOpts d ropts + -- a depth limit will clip and exclude account names later, but should not exclude accounts at this stage nodepthq = dbg1 "nodepthq" $ filterQuery (not . queryIsDepth) q depth = dbg1 "depth" $ queryDepth $ filterQuery queryIsDepth q - matcheddeclaredaccts = dbg1 "matcheddeclaredaccts" $ nub $ sort $ filter (matchesAccount q) $ map fst $ jdeclaredaccounts j - matchedps = dbg1 "ps" $ journalPostings $ filterJournalPostings nodepthq j - matchedusedaccts = dbg1 "matchedusedaccts" $ nub $ sort $ filter (not . T.null) $ map (clipAccountName depth) $ map paccount matchedps - used = boolopt "used" rawopts - declared = boolopt "declared" rawopts - as | declared && not used = matcheddeclaredaccts - | not declared && used = matchedusedaccts - | otherwise = nub $ sort $ matcheddeclaredaccts ++ matchedusedaccts - as' | tree_ ropts = expandAccountNames as - | otherwise = as - render a | tree_ ropts = T.replicate (2 * (accountNameLevel a - 1)) " " <> accountLeafName a - | otherwise = maybeAccountNameDrop ropts a - mapM_ (putStrLn . T.unpack . render) as' + matcheddeclaredaccts = dbg1 "matcheddeclaredaccts" $ filter (matchesAccount nodepthq) $ jdeclaredaccounts j + matchedusedaccts = dbg5 "matchedusedaccts" $ map paccount $ journalPostings $ filterJournalPostings nodepthq j + accts = dbg5 "accts to show" $ -- no need to nub/sort, accountTree will + if | declared && not used -> matcheddeclaredaccts + | not declared && used -> matchedusedaccts + | otherwise -> matcheddeclaredaccts ++ matchedusedaccts + + -- 2. sort them by declaration order and name, at each level of their tree structure + sortedaccts = sortAccountNamesByDeclaration j tree accts + + -- 3. if there's a depth limit, depth-clip and remove any no longer useful items + clippedaccts = + dbg1 "clippedaccts" $ + filter (matchesAccount q) $ -- clipping can leave accounts that no longer visibly match the query + nub $ -- clipping can leave duplicates (adjacent, hopefully) + filter (not . T.null) $ -- depth:0 can leave nulls + map (clipAccountName depth) $ -- clip at depth if specified + sortedaccts + + -- 4. print what remains as a list or tree, maybe applying --drop in the former case + mapM_ (T.putStrLn . render) clippedaccts + where + render a + | tree_ ropts = T.replicate (2 * (accountNameLevel a - 1)) " " <> accountLeafName a + | otherwise = maybeAccountNameDrop ropts a + diff --git a/tests/accounts/sorting.test b/tests/accounts/sorting.test new file mode 100644 index 000000000..b2ba3d10b --- /dev/null +++ b/tests/accounts/sorting.test @@ -0,0 +1,133 @@ +# accounts report sorting. + +# 1. Accounts are sorted alphabetically, at each tree level. +# Flat mode. Unused parent accounts are not added (b). +< +2018/1/1 + (b:j) 1 + +2018/1/1 + (c) 1 + +2018/1/1 + (b:i) 1 + +2018/1/1 + (a:k) 1 + +$ hledger -f- acc +a:k +b:i +b:j +c +>= + + +# 2. Tree mode. Missing parent accounts are added (b). +$ hledger -f- acc --tree +a + k +b + i + j +c +>= + +# 3. With account directives, flat mode. +# At each tree level, declared accounts are sorted first, in +# declaration order, followed by undeclared accounts sorted alphabetically. +# Unused parent accounts are not added (b). +# The b:k, b:j declarations affect the subs of b, not b itself. +< +account b:k +account b:j +account d + +2018/1/1 + (a:l) 1 + +2018/1/1 + (b:i) 1 + +2018/1/1 + (b:j) 1 + +2018/1/1 + (b:k) 1 + +2018/1/1 + (c) 1 + +2018/1/1 + (d) 1 + +$ hledger -f- acc +d +a:l +b:k +b:j +b:i +c +>= + +# 4. With account directives, tree mode. +# Missing parent accounts are added (b). +$ hledger -f- acc --tree +d +a + l +b + k + j + i +c +>= + +# 5. With a depth limit: +# deeper accounts are not excluded +# account names are clipped +# empty clipped names are removed +# duplicate clipped names are removed +# non-matched clipped names are removed. +# Flat mode. +$ hledger -f- acc d b l --depth 1 +d +b +>= + +# # . +# $ hledger -f- acc +# >= + +# # . +# $ hledger -f- acc +# >= + +# # . +# $ hledger -f- acc +# >= + +# # . +# $ hledger -f- acc +# >= + +# # . +# $ hledger -f- acc +# >= + +# # . +# $ hledger -f- acc +# >= + +# # . With --drop: TODO not supported ? +# empty modified names are removed +# duplicate modified names are removed +# non-matched modified names are removed ? +# modified names are sorted somehow ? +# $ hledger -f- acc --drop 1 +# l +# k +# j +# i +# >= + diff --git a/tests/balance/balance.test b/tests/balance/balance.test index 3e33547f9..95ee2ba26 100644 --- a/tests/balance/balance.test +++ b/tests/balance/balance.test @@ -149,21 +149,3 @@ hledger -f - balance -b 2016/10 -e 2016/11 >>>2 >>>= 0 -# not yet implemented, https://github.com/simonmichael/hledger/issues/727 -#7. Sorting by account code. Here, balance should display Equity first. -# hledger -f- bal -N -# <<< -# account Equity 1000 -# account Assets 2000 - -# 2018/1/1 -# (Equity) 1 - -# 2018/1/1 -# (Assets) 1 - -# >>> -# 1 Equity -# 1 Assets -# >>>2 -# >>>=0 diff --git a/tests/balance/sorting.test b/tests/balance/sorting.test new file mode 100644 index 000000000..496e84a73 --- /dev/null +++ b/tests/balance/sorting.test @@ -0,0 +1,352 @@ +* balance report sorting. +# These tests are based on accounts' and somewhat duplicatory if +# sorting code is shared between commands, but might be worth having +# all the same. +** Tabular balance reports +*** Default sort without account declarations + +# 1. Rows are sorted alphabetically by account name, at each tree level. +# Flat mode. Unused parent accounts are not added (b). +< +2018/1/1 + (b:j) 1 + +2018/1/1 + (c) 1 + +2018/1/1 + (b:i) 1 + +2018/1/1 + (a:k) 1 + +$ hledger -f- bal -NY +Balance changes in 2018: + + || 2018 +=====++====== + a:k || 1 + b:i || 1 + b:j || 1 + c || 1 +>= + +# 2. Tree mode. Missing parent accounts are added (b). +$ hledger -f- bal -NY --tree +Balance changes in 2018: + + || 2018 +=====++====== + a || 1 + k || 1 + b || 2 + i || 1 + j || 1 + c || 1 +>= + +*** Default sort with account declarations + +# 3. With account directives, flat mode. +# At each tree level, declared accounts are sorted first, in +# declaration order, followed by undeclared accounts sorted alphabetically. +# Unused parent accounts are not added (b). +# The b:k, b:j declarations affect the subs of b, not b itself. +< +account b:k +account b:j +account d + +2018/1/1 + (a:l) 1 + +2018/1/1 + (b:i) 1 + +2018/1/1 + (b:j) 1 + +2018/1/1 + (b:k) 1 + +2018/1/1 + (c) 1 + +2018/1/1 + (d) 1 + +$ hledger -f- bal -NY +Balance changes in 2018: + + || 2018 +=====++====== + d || 1 + a:l || 1 + b:k || 1 + b:j || 1 + b:i || 1 + c || 1 +>= + +# 4. With account directives, tree mode. +# Missing parent accounts are added (b). +$ hledger -f- bal -NY --tree +Balance changes in 2018: + + || 2018 +=====++====== + d || 1 + a || 1 + l || 1 + b || 3 + k || 1 + j || 1 + i || 1 + c || 1 +>= + +# # . +# < +# $ hledger -f- bal -NY +# >= + +# . With --drop, the modified names are sorted. ? +# XXX not supported ? +# $ hledger -f- bal -NY --drop 2 +# Balance changes in 2018: +# +# || 2018 +# =====++====== +# c || 1 +# i || 1 +# j || 1 +# k || 1 +# >= + +*** Sort by amount + +# 5. Rows are sorted by decreasing amount (and then by account), at each tree level. +# Tree mode. + +< +2018/1/1 + (b:j) 2 + +2018/1/1 + (c) 1 + +2018/1/1 + (b:i) 1 + +2018/1/1 + (a:k) 1 + +$ hledger -f- bal -NY --sort-amount --tree +Balance changes in 2018: + + || 2018 +=====++====== + b || 3 + j || 2 + i || 1 + a || 1 + k || 1 + c || 1 +>= + +# 6. Flat mode. +$ hledger -f- bal -NY --flat --sort-amount +Balance changes in 2018: + + || 2018 +=====++====== + b:j || 2 + a:k || 1 + b:i || 1 + c || 1 +>= + +# 7. When the larger amount is composed of differently-priced amounts, +# it could get sorted as if smaller (bug in hledger 1.4-1.10). Flat mode. +< +2018/1/1 + (a) 2X @ 1Y + (a) 2X @ 2Y + +2018/1/1 + (b) 3X + +$ hledger -f- bal -NY --sort-amount +Balance changes in 2018: + + || 2018 +===++====== + a || 4X + b || 3X +>= + +# 8. Explicit --flat flag, should be the same as above. +$ hledger -f- bal -NY --sort-amount --flat +Balance changes in 2018: + + || 2018 +===++====== + a || 4X + b || 3X +>= + +# 9. Tree mode. +$ hledger -f- bal -NY --sort-amount --tree +Balance changes in 2018: + + || 2018 +===++====== + a || 4X + b || 3X +>= + +** Non-tabular balance reports +*** Default sort without account declarations + +# 10. Rows are sorted alphabetically by account name, at each tree level. +# Tree mode. Missing parent accounts are added (b). +< +2018/1/1 + (b:j) 1 + +2018/1/1 + (c) 1 + +2018/1/1 + (b:i) 1 + +2018/1/1 + (a:k) 1 + +$ hledger -f- bal -N + 1 a:k + 2 b + 1 i + 1 j + 1 c +>= + +# 11. Flat mode. Unused parent accounts are not added (b). +$ hledger -f- bal -N --flat + 1 a:k + 1 b:i + 1 b:j + 1 c +>= + +*** Default sort with account declarations + +# 12. With account directives, tree mode. +# At each tree level, declared accounts are sorted first, in +# declaration order, followed by undeclared accounts sorted alphabetically. +# Missing parent accounts are added (b). +< +account b:k +account b:j +account d + +2018/1/1 + (a:l) 1 + +2018/1/1 + (b:i) 1 + +2018/1/1 + (b:j) 1 + +2018/1/1 + (b:k) 1 + +2018/1/1 + (c) 1 + +2018/1/1 + (d) 1 + +$ hledger -f- bal -N + 1 d + 1 a:l + 3 b + 1 k + 1 j + 1 i + 1 c +>= + +# 13. With account directives, flat mode. +# Unused parent accounts are not added (b). +# The b:k, b:j declarations affect the subs of b, not b itself. +$ hledger -f- bal -N --flat + 1 d + 1 a:l + 1 b:k + 1 b:j + 1 b:i + 1 c +>= + +*** Sort by amount + +# 14. Rows are sorted by decreasing amount (and then by account), at each tree level. +# Tree mode. + +< +2018/1/1 + (b:j) 2 + +2018/1/1 + (c) 1 + +2018/1/1 + (b:i) 1 + +2018/1/1 + (a:k) 1 + +$ hledger -f- bal -N --sort-amount --tree + 3 b + 2 j + 1 i + 1 a:k + 1 c +>= + +# 15. Flat mode. +$ hledger -f- bal -N --flat --sort-amount + 2 b:j + 1 a:k + 1 b:i + 1 c +>= + +# 16. When the larger amount is composed of differently-priced amounts, +# it could get sorted as if smaller (bug in hledger 1.4-1.10). Tree mode. +< +2018/1/1 + (a) 2X @ 1Y + (a) 2X @ 2Y + +2018/1/1 + (b) 3X + +$ hledger -f- bal -N --sort-amount + 4X a + 3X b +>= + +# 17. Explicit --tree flag, should be the same as above. +$ hledger -f- bal -N --sort-amount --tree + 4X a + 3X b +>= + +# 18. Flat mode. +$ hledger -f- bal -N --sort-amount --flat + 4X a + 3X b +>=