1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE NamedFieldPuns #-}
3 {-# LANGUAGE ScopedTypeVariables #-}
4 module Hcompta.Calc.Balance where
7 import qualified Data.Foldable
8 import qualified Data.List
9 import qualified Data.Map.Strict as Data.Map
10 import Data.Map.Strict (Map)
11 import Data.Typeable ()
12 import qualified GHC.Num
14 import qualified Hcompta.Model as Model ()
15 import qualified Hcompta.Model.Account as Account
16 import Hcompta.Model.Account (Account)
17 import qualified Hcompta.Model.Amount as Amount
18 import Hcompta.Model.Amount (Amount, Unit)
19 import qualified Hcompta.Model.Transaction as Transaction
20 import Hcompta.Model.Transaction (Transaction, Posting)
21 import qualified Hcompta.Model.Transaction.Posting as Posting
22 import qualified Hcompta.Model.Journal as Journal
23 import Hcompta.Model.Journal (Journal)
25 -- * The 'Balance' type
27 -- | Sum by 'Account' and sum by 'Unit' of some 'Posting's.
30 { by_account :: By_Account
32 } deriving (Data, Eq, Read, Show, Typeable)
35 = Map Account Account_Sum
36 -- | A sum of 'Amount's,
37 -- concerning a single 'Account'.
42 = Map Amount.Unit Unit_Sum
43 -- | A sum of 'Amount's with their 'Account's involved,
44 -- concerning a single 'Unit'.
47 { amount :: Amount -- ^ The sum of 'Amount's for a single 'Unit'.
48 , accounts :: Map Account () -- ^ The 'Account's involved to build 'amount'.
49 } deriving (Data, Eq, Read, Show, Typeable)
56 { by_account = Data.Map.empty
57 , by_unit = Data.Map.empty
60 nil_By_Account :: By_Account
64 nil_By_Unit :: By_Unit
68 nil_Sum_by_Account :: Account_Sum
72 nil_Sum_by_Unit :: Unit_Sum
75 { accounts = Data.Map.empty
79 -- | Return a tuple associating the given 'Unit_Sum' with its 'Unit'.
80 assoc_unit_sum :: Unit_Sum -> (Unit, Unit_Sum)
81 assoc_unit_sum s = (Amount.unit $ amount s, s)
83 -- | Return a 'Map' associating the given 'Unit_Sum' with their respective 'Unit'.
84 by_Unit_from_List :: [Unit_Sum] -> By_Unit
85 by_Unit_from_List balances =
88 { amount=(GHC.Num.+) (amount x) (amount y)
89 , accounts=Data.Map.union (accounts x) (accounts y)
91 Data.List.map assoc_unit_sum balances
93 -- ** Incremental constructors
95 -- | Return the given 'Balance'
96 -- updated by the given 'Posting'.
97 posting :: Posting -> Balance -> Balance
98 posting post balance =
102 (Data.Map.unionWith (GHC.Num.+))
103 (Posting.account post)
104 (Posting.amounts post)
109 { amount = (GHC.Num.+) (amount x) (amount y)
110 , accounts = Data.Map.union (accounts x) (accounts y)
116 , accounts = Data.Map.singleton (Posting.account post) ()
118 (Posting.amounts post)
121 -- | Return the given 'Balance'
122 -- updated by the 'Transaction.postings'
123 -- of the given 'Transaction'.
124 transaction :: Transaction -> Balance -> Balance
125 transaction tran balance =
127 (flip (Data.List.foldl (flip posting)))
129 (Transaction.postings tran)
131 -- | Return the given 'Balance'
132 -- updated by the 'Transaction.postings'
133 -- and 'Transaction.virtual_postings'
134 -- and 'Transaction.balanced_virtual_postings'
135 -- of the given 'Transaction'.
136 transaction_with_virtual :: Transaction -> Balance -> Balance
137 transaction_with_virtual tran balance =
139 (flip (Data.List.foldl (flip posting)))
141 (Transaction.postings tran)
143 -- | Return the given 'Balance'
144 -- updated by the 'Transaction.balanced_virtual_postings' of the given 'Transaction'.
145 transaction_balanced_virtual :: Transaction -> Balance -> Balance
146 transaction_balanced_virtual tran balance =
148 (flip (Data.List.foldl (flip posting)))
150 (Transaction.balanced_virtual_postings tran)
152 -- | Return the given 'Balance'
153 -- updated by the 'Journal.transactions'
154 -- of the given 'Journal',
155 -- through 'transactions'.
156 journal :: Journal -> Balance -> Balance
157 journal jour balance =
159 (Data.List.foldl (flip transaction))
161 (Journal.transactions jour)
163 -- | Return the given 'Balance'
164 -- updated by the 'Journal.transactions'
165 -- of the given 'Journal',
166 -- through 'transactions'.
167 journal_with_virtual :: Journal -> Balance -> Balance
168 journal_with_virtual jour balance =
170 (Data.List.foldl (flip transaction_with_virtual))
172 (Journal.transactions jour)
174 -- | Return the first given 'Balance'
175 -- updated by the second given 'Balance'.
176 union :: Balance -> Balance -> Balance
181 (Data.Map.unionWith (GHC.Num.+))
187 { amount = (GHC.Num.+) (amount x) (amount y)
188 , accounts = Data.Map.union (accounts x) (accounts y)
194 -- * The 'Equilibre' type
196 -- | See 'equilibre'.
199 deriving (Data, Eq, Read, Show, Typeable)
201 -- | Return the 'by_unit' of the given 'Balance' with:
203 -- * 'Unit's whose 'Unit_Sum' 'amount' verifying 'Amount.is_zero' removed,
205 -- * and remaining 'Unit's having their 'Unit_Sum' 'accounts'
206 -- complemented with the 'by_account' of the given 'Balance'
207 -- (i.e. now mapping to accounts __not__ involved to build the 'Unit_Sum').
208 equilibre :: Balance -> Equilibre
209 equilibre balance = do
210 let max_accounts = Data.Map.size $ by_account balance
211 Equilibre $ Data.Map.foldlWithKey
212 (\m unit Unit_Sum{amount, accounts} ->
213 if Amount.is_zero $ amount
216 case Data.Map.size accounts of
217 n | n == max_accounts ->
218 Data.Map.insert unit Unit_Sum{amount, accounts=Data.Map.empty} m
220 let d = Data.Map.map (const ()) $
221 Data.Map.difference (by_account balance) accounts
222 Data.Map.insert unit Unit_Sum{amount, accounts=d} m
229 -- | Return 'True' if and only if the given 'Equilibre' maps no 'Unit'.
230 is_equilibrated :: Equilibre -> Bool
231 is_equilibrated (Equilibre eq) = Data.Map.null eq
233 -- | Return 'True' if and only if the given 'Equilibre' satisfies:
235 -- * 'is_equilibrated',
236 -- * or 'is_inferrable'.
237 is_equilibrable :: Equilibre -> Bool
238 is_equilibrable e@(Equilibre eq) =
239 Data.Map.null eq || is_inferrable e
241 -- | Return 'True' if and only if the given 'Equilibre'
242 -- maps only to 'Unit_Sum's whose 'accounts'
243 -- maps exactly one 'Account'.
244 is_inferrable :: Equilibre -> Bool
245 is_inferrable (Equilibre eq) =
247 (\Unit_Sum{accounts} -> Data.Map.size accounts == 1)
250 -- | Return 'True' if and only if the given 'Equilibre'
251 -- maps to at least one 'Unit_Sum's whose 'accounts'
252 -- maps more than one 'Account'.
253 is_non_inferrable :: Equilibre -> Bool
254 is_non_inferrable (Equilibre eq) =
256 (\Unit_Sum{accounts} -> Data.Map.size accounts > 1)
259 -- * The 'Expanded' type
263 = Expanded By_Account
264 deriving (Data, Eq, Read, Show, Typeable)
266 -- | Return the given 'By_Account' with:
268 -- * all missing 'Account.ascending' 'Account's inserted,
270 -- * and every mapped Amount.'Amount.By_Unit'
271 -- added with any Amount.'Amount.By_Unit'
272 -- of the 'Account's’ for which it is 'Account.ascending'.
273 expand :: By_Account -> Expanded
275 -- TODO: because (+) is associative
276 -- the complexity could be improved a bit
277 -- by only adding to the longest 'Account.ascending'
278 -- and reuse this result thereafter,
279 -- but coding this requires access
280 -- to the hidden constructors of 'Data.Map.Map',
281 -- which could be done through TemplateHaskell and lens:
282 -- https://stackoverflow.com/questions/24153577/pattern-matching-on-data-map-for-implementing-map
284 -- a0' = a0 + a1 + a2 + a3 <-- current calculus
285 -- = a0 + a1' <-- improved calculus todo
286 -- a1' = a1 + a2 + a3
291 Data.Map.foldrWithKey
293 Account.fold (Account.ascending account)
294 (\prefix -> Data.Map.insertWith (+) prefix amt))