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
129 {- NOTE: overlapping with the instance below.
132 , amount ~ Posting_Amount posting
134 => Consable (Const (Balance_by_Account amount)) posting where
135 mcons p = Const . by_account p . getConst
140 , amount ~ Posting_Amount posting
142 => Consable (Const (Balance_by_Account amount))
143 (foldable posting) where
144 mcons ps (Const !bal) =
145 Const $ Data.Foldable.foldr by_account bal ps
147 -- ** Type 'Balance_by_Unit'
148 newtype Amount amount
149 => Balance_by_Unit amount
150 = Balance_by_Unit (Map (Amount_Unit amount) (Unit_Sum amount))
151 deriving instance ( Amount amount
153 ) => Data (Balance_by_Unit amount)
154 deriving instance ( Amount amount
156 ) => Eq (Balance_by_Unit amount)
157 deriving instance ( Amount amount
159 ) => Show (Balance_by_Unit amount)
160 deriving instance Typeable1 Balance_by_Unit
161 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
163 instance Amount amount
164 => Monoid (Balance_by_Unit amount) where
165 mempty = Balance_by_Unit mempty
166 mappend = union_by_unit
168 -- *** Type 'Unit_Sum'
170 -- | A sum of 'amount's with their 'Account's involved,
171 -- concerning a single 'unit'.
174 { unit_sum_amount :: !amount -- ^ The sum of 'amount's for a single 'unit'.
175 , unit_sum_accounts :: !(Map Account ()) -- ^ The 'Account's involved to build 'unit_sum_amount'.
176 } deriving (Data, Eq, Show, Typeable)
180 empty :: Amount amount => Balance amount
183 { balance_by_account = mempty
184 , balance_by_unit = mempty
187 -- | Return the given 'Balance'
188 -- updated by the given 'Posting'.
191 , balance ~ Balance (Posting_Amount posting) )
192 => posting -> balance -> balance
195 { balance_by_account = by_account post (balance_by_account bal)
196 , balance_by_unit = by_unit post (balance_by_unit bal)
199 -- | Return the given 'Balance'
200 -- updated by the given 'Posting's.
203 , balance ~ Balance (Posting_Amount posting)
204 , Foldable foldable )
205 => foldable posting -> balance -> balance
206 postings = flip (Data.Foldable.foldr balance)
208 -- | Return the first given 'Balance'
209 -- updated by the second given 'Balance'.
210 union :: Amount amount
211 => Balance amount -> Balance amount -> Balance amount
216 { balance_by_account = union_by_account b0a b1a
217 , balance_by_unit = union_by_unit b0u b1u
220 -- | Return the given 'Balance_by_Account'
221 -- updated by the given 'Posting'.
224 , amount ~ Posting_Amount posting
225 , unit ~ Amount_Unit amount
228 -> Balance_by_Account amount
229 -> Balance_by_Account amount
231 Lib.TreeMap.insert mappend
232 (posting_account post)
233 (Account_Sum $ posting_amounts post)
235 -- | Return the given 'Balance_by_Unit'
236 -- updated by the given 'Posting'.
239 , amount ~ Posting_Amount posting
240 , unit ~ Amount_Unit amount )
242 -> Balance_by_Unit amount
243 -> Balance_by_Unit amount
249 { unit_sum_amount = amount
250 , unit_sum_accounts = Data.Map.singleton (posting_account post) ()
252 (posting_amounts post)
254 -- | Return a 'Balance_by_Unit'
255 -- derived from the given 'Balance_by_Account'.
256 by_unit_of_by_account ::
258 , unit ~ Amount_Unit amount
260 => Balance_by_Account amount
261 -> Balance_by_Unit amount
262 -> Balance_by_Unit amount
263 by_unit_of_by_account =
264 flip $ Lib.TreeMap.foldr_with_Path $ curry by_unit
266 -- | Return the first given 'Balance_by_Account'
267 -- updated by the second given 'Balance_by_Account'.
268 union_by_account :: Amount amount
269 => Balance_by_Account amount
270 -> Balance_by_Account amount
271 -> Balance_by_Account amount
272 union_by_account = Lib.TreeMap.union mappend
274 -- | Return the first given 'Balance_by_Unit'
275 -- updated by the second given 'Balance_by_Unit'.
276 union_by_unit :: (Amount amount, unit ~ Amount_Unit amount)
277 => Balance_by_Unit amount
278 -> Balance_by_Unit amount
279 -> Balance_by_Unit amount
282 (Balance_by_Unit a1) =
285 (\new old -> Unit_Sum
286 { unit_sum_amount = amount_add
287 (unit_sum_amount old)
288 (unit_sum_amount new)
289 , unit_sum_accounts = Data.Map.unionWith
291 (unit_sum_accounts old)
292 (unit_sum_accounts new)
296 -- * Type 'Deviation'
298 -- | The 'Balance_by_Unit' whose 'unit_sum_amount'
299 -- is not zero and possible 'Account' to 'infer_equilibrium'.
300 newtype Amount amount
302 = Deviation (Balance_by_Unit amount)
303 deriving instance ( Amount amount
305 ) => Data (Deviation amount)
306 deriving instance ( Amount amount
308 ) => Eq (Deviation amount)
309 deriving instance ( Amount amount
311 ) => Show (Deviation amount)
312 deriving instance Typeable1 Deviation
313 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
315 -- | Return the 'balance_by_unit' of the given 'Balance' with:
317 -- * 'unit's whose 'unit_sum_amount' verifying 'amount_is_zero' removed,
319 -- * and remaining 'unit's having their 'unit_sum_accounts'
320 -- complemented with the 'balance_by_account' of the given 'Balance'
321 -- (i.e. now mapping to 'Account's __not__ involved to build the 'Unit_Sum').
327 { balance_by_account=ba
328 , balance_by_unit=Balance_by_Unit bu
330 let all_accounts = Lib.TreeMap.flatten (const ()) ba
331 let max_accounts = Data.Map.size all_accounts
333 Data.Map.foldlWithKey
334 (\(Balance_by_Unit m) unit Unit_Sum{unit_sum_amount, unit_sum_accounts} ->
336 if amount_null unit_sum_amount
339 case Data.Map.size unit_sum_accounts of
340 n | n == max_accounts ->
341 Data.Map.insert unit Unit_Sum
343 , unit_sum_accounts = Data.Map.empty
346 let diff = Data.Map.difference all_accounts unit_sum_accounts
347 Data.Map.insert unit Unit_Sum
349 , unit_sum_accounts = diff
355 -- ** The equilibrium
357 -- | Return the 'Balance' (adjusted by inferred 'Amount's)
358 -- of the given 'Posting's and either:
360 -- * 'Left': the 'Posting's that cannot be inferred.
361 -- * 'Right': the given 'Posting's with inferred 'Amount's inserted.
364 => Map Account [posting]
365 -> ( Balance (Posting_Amount posting)
366 , Either [Unit_Sum (Posting_Amount posting)] (Map Account [posting])
368 infer_equilibrium posts = do
369 let bal_initial = Data.Foldable.foldr postings empty posts
370 let Deviation (Balance_by_Unit dev) = deviation bal_initial
371 let (bal_adjusted, eithers) =
372 Data.Map.foldrWithKey
373 (\unit unit_sum@(Unit_Sum{unit_sum_amount, unit_sum_accounts})
375 case Data.Map.size unit_sum_accounts of
377 let acct = fst $ Data.Map.elemAt 0 unit_sum_accounts in
378 let amt = amount_negate unit_sum_amount in
379 let amts = Data.Map.singleton unit amt in
380 ( balance (acct, Account_Sum amts) bal
381 , Right (acct, unit, amt) : lr
383 _ -> (bal, Left [unit_sum] : lr))
386 let (l, r) = Lib.Foldable.accumLeftsAndFoldrRights
387 (\(acct, unit, amt) ->
389 (\_new_ps -> insert_amount (unit, amt))
390 acct (assert False []))
393 [] -> (bal_adjusted, Right r)
394 _ -> (bal_adjusted, Left l)
398 => (Amount_Unit (Posting_Amount posting), Posting_Amount posting)
399 -> [posting] -> [posting]
400 insert_amount p@(unit, amt) ps =
402 [] -> assert False []
403 (x:xs) | Data.Map.null (posting_amounts x) ->
404 posting_set_amounts (Data.Map.singleton unit amt) x:xs
405 | Data.Map.notMember unit (posting_amounts x) ->
406 let amts = Data.Map.insertWith
407 (assert False undefined)
408 unit amt (posting_amounts x) in
409 posting_set_amounts amts x:xs
410 (x:xs) -> x:insert_amount p xs
412 -- | Return 'True' if and only if the given 'Deviation' maps no 'unit'.
413 is_at_equilibrium :: Amount amount => Deviation amount -> Bool
414 is_at_equilibrium (Deviation (Balance_by_Unit dev)) = Data.Map.null dev
416 -- | Return 'True' if and only if the given 'Deviation'
417 -- maps only to 'Unit_Sum's whose 'unit_sum_accounts'
418 -- maps exactly one 'Account'.
419 is_equilibrium_inferrable :: Amount amount => Deviation amount -> Bool
420 is_equilibrium_inferrable (Deviation (Balance_by_Unit dev)) =
422 (\Unit_Sum{unit_sum_accounts} -> Data.Map.size unit_sum_accounts == 1)
425 -- | Return 'True' if and only if the given 'Deviation'
426 -- maps to at least one 'Unit_Sum' whose 'unit_sum_accounts'
427 -- maps more than one 'Account'.
428 is_equilibrium_non_inferrable :: Amount amount => Deviation amount -> Bool
429 is_equilibrium_non_inferrable (Deviation (Balance_by_Unit dev)) =
431 (\Unit_Sum{unit_sum_accounts} -> Data.Map.size unit_sum_accounts > 1)
436 -- | Descending propagation of 'Amount's accross 'Account's.
438 = TreeMap Account.Name (Account_Sum_Expanded amount)
439 data Amount amount => Account_Sum_Expanded amount
440 = Account_Sum_Expanded
441 { exclusive :: !(Account_Sum amount)
442 , inclusive :: !(Account_Sum amount) -- ^ 'amount_add' folded over 'exclusive' and 'inclusive' of 'Lib.TreeMap.node_descendants'
444 deriving instance ( Amount amount
446 ) => Data (Account_Sum_Expanded amount)
447 deriving instance ( Amount amount
449 ) => Eq (Account_Sum_Expanded amount)
450 deriving instance ( Amount amount
452 ) => Show (Account_Sum_Expanded amount)
453 deriving instance Typeable1 Account_Sum_Expanded
454 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
456 instance Amount amount => Monoid (Account_Sum_Expanded amount) where
457 mempty = Account_Sum_Expanded mempty mempty
459 (Account_Sum_Expanded e0 i0)
460 (Account_Sum_Expanded e1 i1) =
465 -- | Return the given 'Balance_by_Account' with:
467 -- * all missing 'Account.ascending' 'Account's inserted,
469 -- * and every mapped 'Amount'
470 -- added with any 'Amount'
471 -- of the 'Account's for which it is 'Account.ascending'.
474 => Balance_by_Account amount
477 Lib.TreeMap.map_by_depth_first
478 (\descendants value ->
479 let exclusive = Strict.fromMaybe mempty value in
484 ( flip $ mappend . inclusive
485 . Strict.fromMaybe (assert False undefined)
486 . Lib.TreeMap.node_value)
488 Lib.TreeMap.nodes descendants
491 -- | Return a 'Balance_by_Unit'
492 -- derived from the given 'Expanded' balance.
494 -- NOTE: also correct if the 'Expanded' has been filtered.
495 by_unit_of_expanded ::
497 , unit ~ Amount_Unit amount
500 -> Balance_by_Unit amount
501 -> Balance_by_Unit amount
502 by_unit_of_expanded =
505 go p (Lib.TreeMap.TreeMap m) bal =
506 Data.Map.foldrWithKey
507 (\k Lib.TreeMap.Node{Lib.TreeMap.node_value, Lib.TreeMap.node_descendants} acc ->
509 Strict.Nothing -> go (k:p) node_descendants acc
511 let account = Lib.TreeMap.reverse $ Lib.TreeMap.path k p in
512 by_unit (account, inclusive a) acc)