Skip to content

Introduce multi-period Account data type and use it for MultiBalanceReport and BudgetReport. #2360

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: master
Choose a base branch
from

Conversation

Xitian9
Copy link
Collaborator

@Xitian9 Xitian9 commented Mar 26, 2025

This rejigs the MultiBalanceReport internals to use an enhanced Account data type to save the values. This has a few effects:

  • A small speed improvement on large journal files with interval reporting, due to processing the posting list in one pass.
  • Simplified program logic, as there was a lot of code to convert back and forth between list and tree representations. This can now be removed.
  • Ability to merge Account means that BudgetReport can be simplified.

There are some small changes in behaviour with respect to budget reports, where it looked like some behaviour was implemented to work around needing to get the budget and actuals into the same shape so they could be merged. This is no longer necessary, but may still be desired for other reasons.

Let me know your thoughts.

Xitian9 added 15 commits March 26, 2025 23:31
This upgrades Account to enable it to do the hard work in
MultiBalanceReport, but does not use the new functionality just yet.
It continues to function as before by only using the "abhistorical"
value.
Ensure that implied accounts with no postings are not displayed, but
accounts with zero balance and actual postings are.
Rephrase everything in terms of boringness to make for a clearer logical
flow.
This removes the type alias Account, and replaces it with the
fully-qualified name Account AccountBalance. This breaks some backwards
compatibility, but that was already broken by the change of Account type
constructor in any case. This simplifies the interface.
@Xitian9 Xitian9 force-pushed the multiaccount branch 3 times, most recently from 82a01da to 6745f0a Compare March 27, 2025 12:54
Xitian9 added 2 commits March 28, 2025 20:46
mergeWithKey can create corrupt output if its inputs don't satisfy
certain conditions. We restrict the domain here to only those cases
where it is guaranteed safe. This still covers all the cases that we
need.
@simonmichael simonmichael added needs-review To unblock: needs more code/docs/design review by someone performance Anything performance-related (run time, memory usage, disk space..) labels Apr 2, 2025
Copy link
Owner

@simonmichael simonmichael left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Initial comments.


-- tests

tests_AccountBalance = testGroup "AccountBalance" [
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep these separate for clarity if you like, but I suggest grouping them under tests_AccountBalance for export to avoid breaking the consistent pattern (every module exports one similarly named test group)

@@ -9,7 +9,6 @@ version: 1.42.99
synopsis: Terminal interface for the hledger accounting system
description: A simple terminal user interface for the hledger accounting system.
It can be a more convenient way to browse your accounts than the CLI.
This package currently does not support Microsoft Windows, except in WSL.
.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is in master and will disappear after rebase I think.

account a:ab
account a:ac:aca
account b
# ** 11. In flat mode we can see that undeclared accounts are excluded.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Text trivia:

I have been trying to move in the direction of "list" actually as the preferred term actually (for the -l short flag, eg. Possibly should go all the way and add --list long flag ?)

But your change matches the flag used in the command, so maybe it's better..

I feel maybe the old test description here was bad.. I think --declared includes declared accounts, rather than excluding undeclared ones. Perhaps we should drop that text.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with the behaviour change here personally. I'd put a ! (= "breaking change") after the first commit message prefix which helps me notice these at changelog/relnotes time.

ref: is a new prefix meaning "refactoring" I guess. Ok, but perhaps it isn't the right one here since this is not strictly a refactoring. In such cases I often go with a generic dev:.


# ** 16. balance --flat --empty does not display accounts which have not been
# seen, even if they're implied, but does show accounts that have been seen
# with 0 balance.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what this means in userese ?

@@ -50,7 +50,6 @@ Budget performance in 2016-12-01..2016-12-03:
|| 2016-12-01 2016-12-02 2016-12-03
==================++==============================================================
assets:cash || $-10 [ 40% of $-25] $-14 [56% of $-25] $-51 [204% of $-25]
expenses || $10 [ 40% of $25] $14 [56% of $25] $51 [204% of $25]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another behaviour change, worth noting with a ! in message.

The parent "expenses" account is not shown, because there's no explicit budget goal for it, and because we're in list mode ? So if we want to see aggregated budget performance, tree mode will be needed. Ok I guess.

Why is it still shown in the previous test ?

@@ -747,8 +745,7 @@ Budget performance in 2023-08:
==============++=================
income || 0 [0% of $-200]
employment || 0 [0% of $-100]
gifts || 0
cash || 0 [0% of $-100]
gifts:cash || 0 [0% of $-100]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a consistency fix, squashing these like a normal balance report would. I think I tried and failed to fix this recently. 👍🏻

@@ -296,6 +296,7 @@ Income Statement ..
=================++====
Revenues ||
-----------------++----
revenues || 0
Copy link
Owner

@simonmichael simonmichael Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should drop "leaf" from this test's description now I guess.

data AccountBalances a = AccountBalances {
abhistorical :: a -- ^ historical balance information
,abdatemap :: IM.IntMap a -- ^ balance information associated to a start day
} deriving (Eq, Functor, Generic)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for using a type parameter ? Do we truly need it ?

How does the "start day" map, with Int keys I assume, work here ?

Copy link
Owner

@simonmichael simonmichael Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for using a type parameter ? Do we truly need it ?

For budget report, I guess. It hurts code comprehensability a bit.

@@ -733,18 +735,31 @@ nullaccountdeclarationinfo = AccountDeclarationInfo {

-- | An account, with its balances, parent/subaccount relationships, etc.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should change this to "An account within a hierarchy", or something. Representing/navigating a hierarchy of account balances is Account's purpose.

@simonmichael simonmichael added balance balancesheet incomestatement and removed needs-review To unblock: needs more code/docs/design review by someone labels Apr 3, 2025
@simonmichael simonmichael added cashflow budget The balance command's --budget report balancesheetequity labels Apr 3, 2025
@simonmichael
Copy link
Owner

A small speed improvement on large journal files

Could we quantify that a little more - eg "balance reports are 1% faster with 1k txns, 5% faster with 10k txns" ?

@simonmichael
Copy link
Owner

Also I wonder if there's any memory impact, stats might be a quick way to check (it runs balance reports probably)

instance Monoid a => Monoid (AccountBalances a) where
mempty = AccountBalances mempty mempty

-- | Construct an 'AccountBalance' from a list.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Construct an 'AccountBalances' from a list

accountBalancesFromList :: a -> [(Day, a)] -> AccountBalances a
accountBalancesFromList h = AccountBalances h . IM.fromList . map (\(d, a) -> (fromInteger $ toModifiedJulianDay d, a))

-- | Get the 'AccountBalance' associated to the period containing a given 'Day'.
Copy link
Owner

@simonmichael simonmichael Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More general doc: Get the account balance information associated ... ?

lookupAccountBalance d (AccountBalances h as) =
maybe h snd $ IM.lookupLE (fromInteger $ toModifiedJulianDay d) as

-- | Add the 'AccountBalance' to the appropriate location in 'AccountBalances'.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add balance information ...

{-|


An 'AccountBalance' consists of a historical balance, along with balances
Copy link
Owner

@simonmichael simonmichael Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe clarify that this module contains operations for two distinct types: AccountBalances and AccountBalance. (Easy for a reader to mix up). Almost worth separate modules ?

Nothing -> balances{abhistorical = abhistorical balances <> b}
Just day -> balances{abdatemap = IM.insertWith (<>) (fromInteger $ toModifiedJulianDay day) b $ abdatemap balances}

-- | Performs an operation on the contents of two 'AccountBalances'.
Copy link
Owner

@simonmichael simonmichael Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merges two AccountBalances, using the given operation to combine their balance informations. ?


-- | Performs an operation on the contents of two 'AccountBalances', with
-- separate treatments for those only in the first, only in the second, or in
-- both 'AccountBalance's.
Copy link
Owner

@simonmichael simonmichael Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merges two AccountBalances, with separate operations for balance informations only in ... ?

applyAccountBalance :: (MixedAmount -> MixedAmount) -> AccountBalance -> AccountBalance
applyAccountBalance f a = a{abebalance = f $ abebalance a, abibalance = f $ abibalance a}

-- | Perform an operation on the 'MixedAmount' in two 'AccountBalance'.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge two AccountBalance, using the given operation to combine their amounts ?

,aebalance :: MixedAmount -- ^ this account's balance, excluding subaccounts
,aibalance :: MixedAmount -- ^ this account's balance, including subaccounts
} deriving (Generic)
,abalances :: AccountBalances a -- ^ historical and date-associated account balances
Copy link
Owner

@simonmichael simonmichael Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's hard to be clear on this everywhere, but maybe here mention both "end balances" (on a date) and "balance changes" (in a period) (consistent with terminology elsewhere) are/may be being represented by "AccountBalance(s)".

aname=a
,asubs=map (uncurry accountTree') $ M.assocs m'
}
(accountFromBalances a bals){ asubs=map (uncurry accountTree') $ M.assocs m' }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc and function is a bit hard to understand, any clarification welcome

mapAccounts f a = f a{asubs = map (mapAccounts f) $ asubs a}

-- | Apply a function to all 'AccountBalances' within this and sub accounts.
Copy link
Owner

@simonmichael simonmichael Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar functions in AccountBalances.hs are named "apply" - maybe "map" is best, like this one ?

allAccounts p a
| not (p a) = False
| otherwise = all (allAccounts p) $ asubs a

-- | Add subaccount-inclusive balances to an account tree.
Copy link
Owner

@simonmichael simonmichael Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you're here, we could make this one clearer:
Recalculate all the subaccount-inclusive balances in this account tree.

subtotals = foldMap abalances subs

setImplicitBalances :: AccountBalances AccountBalance -> AccountBalances AccountBalance
setImplicitBalances = mergeAccountBalances onlyChildren noChildren combineChildren subtotals
Copy link
Owner

@simonmichael simonmichael Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean setInclusiveBalances ?

-- sort first, and vice versa.
sortAccountTreeByAmount :: NormalSign -> Account -> Account
sortAccountTreeByAmount normalsign = mapAccounts $ \a -> a{asubs=sortSubs $ asubs a}
-- | Merge two accounts and their subaccounts.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge two account trees ?

-- | Derive 1. an account tree and 2. each account's total exclusive
-- and inclusive changes from a list of postings.
-- This is the core of the balance command (and of *ledger).
-- The accounts are returned as tree.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a tree

@@ -85,33 +108,47 @@ nullacct = Account
-- The accounts are returned as a list in flattened tree order,
-- and also reference each other as a tree.
-- (The first account is the root of the tree.)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this (and maybe the next), let's explain a bit more that this calculates multiple inclusive and exclusive change amounts for each account, associated with the dates produced by the given date-bucketing function (usually representing the end dates of report subperiods)

Copy link
Owner

@simonmichael simonmichael Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(It looks like this will enable non-contiguous report periods in future, which will be cool)

@Xitian9
Copy link
Collaborator Author

Xitian9 commented Apr 5, 2025 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
balance balancesheet balancesheetequity budget The balance command's --budget report cashflow incomestatement performance Anything performance-related (run time, memory usage, disk space..)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants