1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE ScopedTypeVariables #-}
3 module Hcompta.Calc.Balance where
6 import qualified Data.Foldable
7 import qualified Data.List
8 import qualified Data.Map.Strict as Data.Map
9 import Data.Map.Strict (Map)
10 import Data.Typeable ()
11 import qualified GHC.Num
13 import qualified Hcompta.Model as Model ()
14 import qualified Hcompta.Model.Account as Account
15 import Hcompta.Model.Account (Account)
16 import qualified Hcompta.Model.Amount as Amount
17 import Hcompta.Model.Amount (Amount, Unit)
18 import qualified Hcompta.Model.Transaction as Transaction
19 import Hcompta.Model.Transaction (Transaction, Posting)
20 import qualified Hcompta.Model.Transaction.Posting as Posting
21 import qualified Hcompta.Model.Journal as Journal
22 import Hcompta.Model.Journal (Journal)
24 -- * The 'Balance' type
26 -- | Sum by 'Account' and sum by 'Unit' of some 'Posting's.
29 { by_account :: By_Account
31 } deriving (Data, Eq, Read, Show, Typeable)
33 = Map Account Sum_by_Account
35 = Map Amount.Unit Sum_by_Unit
38 -- | A sum by 'Account' of the 'Amount's of some 'Posting's.
42 -- | A sum by 'Unit' of the 'Amount's of some 'Posting's,
43 -- with the 'Account's involved to build that sum.
46 { accounts :: Map Account () -- ^ The 'Account's involved to build 'amount'.
47 , amount :: Amount -- ^ The sum of 'Amount's for a same 'Unit'.
48 } deriving (Data, Eq, Read, Show, Typeable)
55 { by_account = Data.Map.empty
56 , by_unit = Data.Map.empty
59 nil_By_Account :: By_Account
63 nil_By_Unit :: By_Unit
67 nil_Sum_by_Account :: Sum_by_Account
71 nil_Sum_by_Unit :: Sum_by_Unit
74 { accounts = Data.Map.empty
78 -- | Return a tuple associating the given 'Sum_by_Unit' with its 'Unit'.
79 assoc_by_amount_unit :: Sum_by_Unit -> (Unit, Sum_by_Unit)
80 assoc_by_amount_unit s = (Amount.unit $ amount s, s)
82 -- | Return a 'Map' associating the given 'Sum_by_Unit' with their respective 'Unit'.
83 by_Unit_from_List :: [Sum_by_Unit] -> By_Unit
84 by_Unit_from_List balances =
87 { amount=(GHC.Num.+) (amount x) (amount y)
88 , accounts=Data.Map.union (accounts x) (accounts y)
90 Data.List.map assoc_by_amount_unit balances
92 -- ** Incremental constructors
94 -- | Return the given 'Balance'
95 -- updated by the given 'Posting'.
96 posting :: Posting -> Balance -> Balance
97 posting post balance =
101 (Data.Map.unionWith (GHC.Num.+))
102 (Posting.account post)
103 (Posting.amounts post)
108 { amount = (GHC.Num.+) (amount x) (amount y)
109 , accounts = Data.Map.union (accounts x) (accounts y)
115 , accounts=Data.Map.singleton (Posting.account post) ()
117 (Posting.amounts post)
120 -- | Return the given 'Balance'
121 -- updated by the 'Transaction.postings' of the given 'Transaction'.
122 transaction :: Transaction -> Balance -> Balance
123 transaction tran balance =
125 (flip (Data.List.foldl (flip posting)))
127 (Transaction.postings tran)
129 -- | Return the given 'Balance'
130 -- updated by the 'Journal.transactions' of the given 'Journal'.
131 journal :: Journal -> Balance -> Balance
132 journal jour balance =
134 (Data.List.foldl (flip transaction))
136 (Journal.transactions jour)
138 -- | Return the first given 'Balance'
139 -- updated by the second given 'Balance'.
140 union :: Balance -> Balance -> Balance
145 (Data.Map.unionWith (GHC.Num.+))
151 { amount = (GHC.Num.+) (amount x) (amount y)
152 , accounts = Data.Map.union (accounts x) (accounts y)
160 -- | Return 'True' if and only if the 'Balance'
161 -- has all its 'by_unit' 'amount's verify 'Amount.is_zero'
162 -- or exactly one 'Account' of the 'by_unit' 'accounts' is not in 'by_account'.
163 is_equilibrated :: Balance -> Bool
164 is_equilibrated balance =
167 (Amount.is_zero $ amount s) ||
168 (Data.Map.size (accounts s) ==
169 (Data.Map.size (by_account balance) - 1))) -- NOTE: Data.Map.size is O(1)
172 -- * The 'Expanded' type
175 = Expanded By_Account
176 deriving (Data, Eq, Read, Show, Typeable)
178 -- | Return the given 'By_Account'
179 -- with all missing 'Account.ascending' 'Account's inserted,
180 -- and every mapped Amount.'Amount.By_Unit'
181 -- added with any 'Account's Amount.'Amount.By_Unit'
182 -- to which it is 'Account.ascending'.
183 expand :: By_Account -> Expanded
185 -- TODO: because (+) is associative
186 -- the complexity could be improved a bit
187 -- by only adding to the longest 'Account.ascending'
188 -- and reuse this result thereafter,
189 -- but coding this requires access
190 -- to the hidden constructors of 'Data.Map.Map',
191 -- which could be done through TemplateHaskell and lens:
192 -- https://stackoverflow.com/questions/24153577/pattern-matching-on-data-map-for-implementing-map
194 -- a0' = a0 + a1 + a2 + a3 <-- current calculus
195 -- = a0 + a1' <-- improved calculus todo
196 -- a1' = a1 + a2 + a3
201 Data.Map.foldrWithKey
203 Account.fold (Account.ascending account)
204 (\prefix -> Data.Map.insertWith (+) prefix amt))