1 {-# LANGUAGE BangPatterns #-}
2 {-# LANGUAGE DeriveDataTypeable #-}
3 {-# LANGUAGE FlexibleContexts #-}
4 {-# LANGUAGE FlexibleInstances #-}
5 {-# LANGUAGE MultiParamTypeClasses #-}
6 {-# LANGUAGE NamedFieldPuns #-}
7 {-# LANGUAGE StandaloneDeriving #-}
8 {-# LANGUAGE TypeFamilies #-}
9 {-# OPTIONS_GHC -fno-warn-deprecations #-} -- FIXME: to be removed when dropping GHC-7.6 support
10 module Hcompta.Balance where
12 import Control.Applicative (Const(..))
13 import Control.Exception (assert)
15 import qualified Data.Foldable
16 -- import Data.Foldable (Foldable(..))
17 import qualified Data.Map.Strict as Data.Map
18 import Data.Map.Strict (Map)
19 import qualified Data.Strict.Maybe as Strict
20 import Data.Typeable ()
22 import Hcompta.Lib.Consable (Consable(..))
23 import qualified Hcompta.Lib.Foldable as Lib.Foldable
24 import qualified Hcompta.Lib.TreeMap as Lib.TreeMap
25 import Hcompta.Lib.TreeMap (TreeMap)
26 import qualified Hcompta.Account as Account
27 import Hcompta.Account (Account)
29 -- * Requirements' interface
33 ( Data (Amount_Unit a)
35 , Show (Amount_Unit a)
36 , Typeable (Amount_Unit a)
39 amount_null :: a -> Bool
40 amount_add :: a -> a -> a
41 amount_negate :: a -> a
45 -- | A 'posting' used to produce a 'Balance'
46 -- must be an instance of this class.
47 class Amount (Posting_Amount p) => Posting p where
49 posting_account :: p -> Account
50 posting_amounts :: p -> Map (Amount_Unit (Posting_Amount p)) (Posting_Amount p)
51 posting_set_amounts :: Map (Amount_Unit (Posting_Amount p)) (Posting_Amount p) -> p -> p
54 instance (Amount amount, unit ~ Amount_Unit amount)
55 => Posting (Account, Map unit amount)
57 type Posting_Amount (Account, Map unit amount) = amount
60 posting_set_amounts amounts (acct, _) = (acct, amounts)
63 instance (Amount amount)
64 => Posting (Account, Account_Sum amount)
66 type Posting_Amount (Account, Account_Sum amount) = amount
68 posting_amounts (_, Account_Sum x) = x
69 posting_set_amounts amounts (acct, _) = (acct, Account_Sum amounts)
73 -- | Sum by 'Account' and sum by 'unit' of some 'Posting's.
77 { balance_by_account :: !(Balance_by_Account amount)
78 , balance_by_unit :: !(Balance_by_Unit amount)
80 deriving instance ( Amount amount
82 ) => Data (Balance amount)
83 deriving instance ( Amount amount
85 ) => Eq (Balance amount)
86 deriving instance ( Amount amount
88 ) => Show (Balance amount)
89 deriving instance Typeable1 Balance
90 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
92 instance Amount amount => Monoid (Balance amount) where
96 -- ** Type 'Balance_by_Account'
97 type Balance_by_Account amount
98 = TreeMap Account.Name
101 -- *** Type 'Account_Sum'
102 -- | A sum of 'amount's,
103 -- concerning a single 'Account'.
104 newtype Amount amount
105 => Account_Sum amount
106 = Account_Sum (Map (Amount_Unit amount) amount)
107 get_Account_Sum :: Amount amount => Account_Sum amount -> Map (Amount_Unit amount) amount
108 get_Account_Sum (Account_Sum m) = m
109 deriving instance ( Amount amount
111 ) => Data (Account_Sum amount)
112 deriving instance ( Amount amount
114 ) => Eq (Account_Sum amount)
115 deriving instance ( Amount amount
117 ) => Show (Account_Sum amount)
118 deriving instance Typeable1 Account_Sum
119 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
121 instance Amount amount
122 => Monoid (Account_Sum amount) where
123 mempty = Account_Sum mempty
127 Account_Sum $ Data.Map.unionWith amount_add a0 a1
131 , amount ~ Posting_Amount posting
133 => Consable (Const (Balance_by_Account amount)) posting where
134 mcons p (Const !bal) = Const $ cons_by_account p bal
138 , amount ~ Posting_Amount posting
140 => Consable (Const (Balance_by_Account amount))
141 (foldable posting) where
142 mcons ps (Const !bal) =
143 Const $ Data.Foldable.foldr cons_by_account bal ps
145 -- ** Type 'Balance_by_Unit'
146 newtype Amount amount
147 => Balance_by_Unit amount
148 = Balance_by_Unit (Map (Amount_Unit amount) (Unit_Sum amount))
149 deriving instance ( Amount amount
151 ) => Data (Balance_by_Unit amount)
152 deriving instance ( Amount amount
154 ) => Eq (Balance_by_Unit amount)
155 deriving instance ( Amount amount
157 ) => Show (Balance_by_Unit amount)
158 deriving instance Typeable1 Balance_by_Unit
159 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
161 instance Amount amount
162 => Monoid (Balance_by_Unit amount) where
163 mempty = Balance_by_Unit mempty
164 mappend = union_by_unit
166 -- *** Type 'Unit_Sum'
168 -- | A sum of 'amount's with their 'Account's involved,
169 -- concerning a single 'unit'.
172 { unit_sum_amount :: !amount -- ^ The sum of 'amount's for a single 'unit'.
173 , unit_sum_accounts :: !(Map Account ()) -- ^ The 'Account's involved to build 'unit_sum_amount'.
174 } deriving (Data, Eq, Show, Typeable)
178 empty :: Amount amount => Balance amount
181 { balance_by_account = mempty
182 , balance_by_unit = mempty
185 -- | Return the given 'Balance'
186 -- updated by the given 'Posting'.
189 , balance ~ Balance (Posting_Amount posting) )
190 => posting -> balance -> balance
193 { balance_by_account = cons_by_account post (balance_by_account bal)
194 , balance_by_unit = cons_by_unit post (balance_by_unit bal)
197 -- | Return the given 'Balance'
198 -- updated by the given 'Posting's.
201 , balance ~ Balance (Posting_Amount posting)
202 , Foldable foldable )
203 => foldable posting -> balance -> balance
204 postings = flip (Data.Foldable.foldr cons)
206 -- | Return the first given 'Balance'
207 -- updated by the second given 'Balance'.
208 union :: Amount amount
209 => Balance amount -> Balance amount -> Balance amount
214 { balance_by_account = union_by_account b0a b1a
215 , balance_by_unit = union_by_unit b0u b1u
218 -- | Return the given 'Balance_by_Account'
219 -- updated by the given 'Posting'.
222 , amount ~ Posting_Amount posting
223 , unit ~ Amount_Unit amount
226 -> Balance_by_Account amount
227 -> Balance_by_Account amount
228 cons_by_account post =
229 Lib.TreeMap.insert mappend
230 (posting_account post)
231 (Account_Sum $ posting_amounts post)
233 -- | Return the given 'Balance_by_Unit'
234 -- updated by the given 'Posting'.
237 , amount ~ Posting_Amount posting
238 , unit ~ Amount_Unit amount )
240 -> Balance_by_Unit amount
241 -> Balance_by_Unit amount
247 { unit_sum_amount = amount
248 , unit_sum_accounts = Data.Map.singleton (posting_account post) ()
250 (posting_amounts post)
252 -- | Return a 'Balance_by_Unit'
253 -- derived from the given 'Balance_by_Account'.
254 by_unit_of_by_account ::
256 , unit ~ Amount_Unit amount
258 => Balance_by_Account amount
259 -> Balance_by_Unit amount
260 -> Balance_by_Unit amount
261 by_unit_of_by_account =
262 flip $ Lib.TreeMap.foldr_with_Path $ curry cons_by_unit
264 -- | Return the first given 'Balance_by_Account'
265 -- updated by the second given 'Balance_by_Account'.
266 union_by_account :: Amount amount
267 => Balance_by_Account amount
268 -> Balance_by_Account amount
269 -> Balance_by_Account amount
270 union_by_account = Lib.TreeMap.union mappend
272 -- | Return the first given 'Balance_by_Unit'
273 -- updated by the second given 'Balance_by_Unit'.
274 union_by_unit :: (Amount amount, unit ~ Amount_Unit amount)
275 => Balance_by_Unit amount
276 -> Balance_by_Unit amount
277 -> Balance_by_Unit amount
280 (Balance_by_Unit a1) =
283 (\new old -> Unit_Sum
284 { unit_sum_amount = amount_add
285 (unit_sum_amount old)
286 (unit_sum_amount new)
287 , unit_sum_accounts = Data.Map.unionWith
289 (unit_sum_accounts old)
290 (unit_sum_accounts new)
294 -- * Type 'Deviation'
296 -- | The 'Balance_by_Unit' whose 'unit_sum_amount'
297 -- is not zero and possible 'Account' to 'infer_equilibrium'.
298 newtype Amount amount
300 = Deviation (Balance_by_Unit amount)
301 deriving instance ( Amount amount
303 ) => Data (Deviation amount)
304 deriving instance ( Amount amount
306 ) => Eq (Deviation amount)
307 deriving instance ( Amount amount
309 ) => Show (Deviation amount)
310 deriving instance Typeable1 Deviation
311 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
313 -- | Return the 'balance_by_unit' of the given 'Balance' with:
315 -- * 'unit's whose 'unit_sum_amount' verifying 'amount_is_zero' removed,
317 -- * and remaining 'unit's having their 'unit_sum_accounts'
318 -- complemented with the 'balance_by_account' of the given 'Balance'
319 -- (i.e. now mapping to 'Account's __not__ involved to build the 'Unit_Sum').
325 { balance_by_account=ba
326 , balance_by_unit=Balance_by_Unit bu
328 let all_accounts = Lib.TreeMap.flatten (const ()) ba
329 let max_accounts = Data.Map.size all_accounts
331 Data.Map.foldlWithKey
332 (\(Balance_by_Unit m) unit Unit_Sum{unit_sum_amount, unit_sum_accounts} ->
334 if amount_null unit_sum_amount
337 case Data.Map.size unit_sum_accounts of
338 n | n == max_accounts ->
339 Data.Map.insert unit Unit_Sum
341 , unit_sum_accounts = Data.Map.empty
344 let diff = Data.Map.difference all_accounts unit_sum_accounts
345 Data.Map.insert unit Unit_Sum
347 , unit_sum_accounts = diff
353 -- ** The equilibrium
355 -- | Return the 'Balance' (adjusted by inferred 'Amount's)
356 -- of the given 'Posting's and either:
358 -- * 'Left': the 'Posting's that cannot be inferred.
359 -- * 'Right': the given 'Posting's with inferred 'Amount's inserted.
362 => Map Account [posting]
363 -> ( Balance (Posting_Amount posting)
364 , Either [Unit_Sum (Posting_Amount posting)] (Map Account [posting])
366 infer_equilibrium posts = do
367 let bal_initial = Data.Foldable.foldr postings empty posts
368 let Deviation (Balance_by_Unit dev) = deviation bal_initial
369 let (bal_adjusted, eithers) =
370 Data.Map.foldrWithKey
371 (\unit unit_sum@(Unit_Sum{unit_sum_amount, unit_sum_accounts})
373 case Data.Map.size unit_sum_accounts of
375 let acct = fst $ Data.Map.elemAt 0 unit_sum_accounts in
376 let amt = amount_negate unit_sum_amount in
377 let amts = Data.Map.singleton unit amt in
378 ( cons (acct, Account_Sum amts) bal
379 , Right (acct, unit, amt) : lr
381 _ -> (bal, Left [unit_sum] : lr))
384 let (l, r) = Lib.Foldable.accumLeftsAndFoldrRights
385 (\(acct, unit, amt) ->
387 (\_new_ps -> insert_amount (unit, amt))
388 acct (assert False []))
391 [] -> (bal_adjusted, Right r)
392 _ -> (bal_adjusted, Left l)
396 => (Amount_Unit (Posting_Amount posting), Posting_Amount posting)
397 -> [posting] -> [posting]
398 insert_amount p@(unit, amt) ps =
400 [] -> assert False []
401 (x:xs) | Data.Map.null (posting_amounts x) ->
402 posting_set_amounts (Data.Map.singleton unit amt) x:xs
403 | Data.Map.notMember unit (posting_amounts x) ->
404 let amts = Data.Map.insertWith
405 (assert False undefined)
406 unit amt (posting_amounts x) in
407 posting_set_amounts amts x:xs
408 (x:xs) -> x:insert_amount p xs
410 -- | Return 'True' if and only if the given 'Deviation' maps no 'unit'.
411 is_at_equilibrium :: Amount amount => Deviation amount -> Bool
412 is_at_equilibrium (Deviation (Balance_by_Unit dev)) = Data.Map.null dev
414 -- | Return 'True' if and only if the given 'Deviation'
415 -- maps only to 'Unit_Sum's whose 'unit_sum_accounts'
416 -- maps exactly one 'Account'.
417 is_equilibrium_inferrable :: Amount amount => Deviation amount -> Bool
418 is_equilibrium_inferrable (Deviation (Balance_by_Unit dev)) =
420 (\Unit_Sum{unit_sum_accounts} -> Data.Map.size unit_sum_accounts == 1)
423 -- | Return 'True' if and only if the given 'Deviation'
424 -- maps to at least one 'Unit_Sum' whose 'unit_sum_accounts'
425 -- maps more than one 'Account'.
426 is_equilibrium_non_inferrable :: Amount amount => Deviation amount -> Bool
427 is_equilibrium_non_inferrable (Deviation (Balance_by_Unit dev)) =
429 (\Unit_Sum{unit_sum_accounts} -> Data.Map.size unit_sum_accounts > 1)
434 -- | Descending propagation of 'Amount's accross 'Account's.
436 = TreeMap Account.Name (Account_Sum_Expanded amount)
437 data Amount amount => Account_Sum_Expanded amount
438 = Account_Sum_Expanded
439 { exclusive :: !(Account_Sum amount)
440 , inclusive :: !(Account_Sum amount) -- ^ 'amount_add' folded over 'exclusive' and 'inclusive' of 'Lib.TreeMap.node_descendants'
442 deriving instance ( Amount amount
444 ) => Data (Account_Sum_Expanded amount)
445 deriving instance ( Amount amount
447 ) => Eq (Account_Sum_Expanded amount)
448 deriving instance ( Amount amount
450 ) => Show (Account_Sum_Expanded amount)
451 deriving instance Typeable1 Account_Sum_Expanded
452 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
454 instance Amount amount => Monoid (Account_Sum_Expanded amount) where
455 mempty = Account_Sum_Expanded mempty mempty
457 (Account_Sum_Expanded e0 i0)
458 (Account_Sum_Expanded e1 i1) =
463 -- | Return the given 'Balance_by_Account' with:
465 -- * all missing 'Account.ascending' 'Account's inserted,
467 -- * and every mapped 'Amount'
468 -- added with any 'Amount'
469 -- of the 'Account's for which it is 'Account.ascending'.
472 => Balance_by_Account amount
475 Lib.TreeMap.map_by_depth_first
476 (\descendants value ->
477 let exclusive = Strict.fromMaybe mempty value in
482 ( flip $ mappend . inclusive
483 . Strict.fromMaybe (assert False undefined)
484 . Lib.TreeMap.node_value)
486 Lib.TreeMap.nodes descendants
489 -- | Return a 'Balance_by_Unit'
490 -- derived from the given 'Expanded' balance.
492 -- NOTE: also correct if the 'Expanded' has been filtered.
493 by_unit_of_expanded ::
495 , unit ~ Amount_Unit amount
498 -> Balance_by_Unit amount
499 -> Balance_by_Unit amount
500 by_unit_of_expanded =
503 go p (Lib.TreeMap.TreeMap m) bal =
504 Data.Map.foldrWithKey
505 (\k Lib.TreeMap.Node{Lib.TreeMap.node_value, Lib.TreeMap.node_descendants} acc ->
507 Strict.Nothing -> go (k:p) node_descendants acc
509 let account = Lib.TreeMap.reverse $ Lib.TreeMap.path k p in
510 cons_by_unit (account, inclusive a) acc)