1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE FlexibleContexts #-}
3 {-# LANGUAGE FlexibleInstances #-}
4 {-# LANGUAGE NamedFieldPuns #-}
5 {-# LANGUAGE StandaloneDeriving #-}
6 {-# LANGUAGE TypeFamilies #-}
7 {-# OPTIONS_GHC -fno-warn-deprecations #-} -- FIXME: to be removed when dropping GHC-7.6 support
8 module Hcompta.Balance where
10 import Control.Exception (assert)
12 import qualified Data.Foldable
13 import Data.Foldable (Foldable(..))
14 import qualified Data.Map.Strict as Data.Map
15 import Data.Map.Strict (Map)
16 import Data.Maybe (fromMaybe)
17 import Data.Typeable ()
19 import qualified Hcompta.Lib.Foldable as Lib.Foldable
20 import qualified Hcompta.Lib.TreeMap as Lib.TreeMap
21 import Hcompta.Lib.TreeMap (TreeMap)
22 import qualified Hcompta.Account as Account
23 import Hcompta.Account (Account)
25 -- * Requirements' interface
29 ( Data (Amount_Unit a)
31 , Show (Amount_Unit a)
32 , Typeable (Amount_Unit a)
35 amount_null :: a -> Bool
36 amount_add :: a -> a -> a
37 amount_negate :: a -> a
41 -- | A 'posting' used to produce a 'Balance'
42 -- must be an instance of this class.
43 class Amount (Posting_Amount p) => Posting p where
45 posting_account :: p -> Account
46 posting_amounts :: p -> Map (Amount_Unit (Posting_Amount p)) (Posting_Amount p)
47 posting_set_amounts :: Map (Amount_Unit (Posting_Amount p)) (Posting_Amount p) -> p -> p
49 instance (Amount amount, unit ~ Amount_Unit amount)
50 => Posting (Account, Map unit amount)
52 type Posting_Amount (Account, Map unit amount) = amount
55 posting_set_amounts amounts (acct, _) = (acct, amounts)
59 -- | Sum by 'Account' and sum by 'unit' of some 'Posting's.
60 data Amount amount => Balance amount
62 { balance_by_account :: Balance_by_Account amount (Amount_Unit amount)
63 , balance_by_unit :: Balance_by_Unit amount (Amount_Unit amount)
65 deriving instance ( Amount amount
67 ) => Data (Balance amount)
68 deriving instance ( Amount amount
70 ) => Eq (Balance amount)
71 deriving instance ( Amount amount
73 ) => Show (Balance amount)
74 deriving instance Typeable1 Balance
75 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
77 type Balance_by_Account amount unit
78 = TreeMap Account.Name
79 (Account_Sum amount unit)
81 -- | A sum of 'amount's,
82 -- concerning a single 'Account'.
83 type Account_Sum amount unit
84 = Data.Map.Map unit amount
86 type Balance_by_Unit amount unit
87 = Map unit (Unit_Sum amount)
89 -- | A sum of 'amount's with their 'Account's involved,
90 -- concerning a single 'unit'.
93 { unit_sum_amount :: amount -- ^ The sum of 'amount's for a single 'unit'.
94 , unit_sum_accounts :: Map Account () -- ^ The 'Account's involved to build 'unit_sum_amount'.
95 } deriving (Data, Eq, Show, Typeable)
99 nil :: Amount amount => Balance amount
102 { balance_by_account = Lib.TreeMap.empty
103 , balance_by_unit = Data.Map.empty
106 -- | Return the given 'Balance'
107 -- updated by the given 'Posting'.
110 , balance ~ Balance (Posting_Amount posting) )
111 => posting -> balance -> balance
114 { balance_by_account = by_account post (balance_by_account bal)
115 , balance_by_unit = by_unit post (balance_by_unit bal)
118 -- | Return the given 'Balance'
119 -- updated by the given 'Posting's.
122 , balance ~ Balance (Posting_Amount posting)
123 , Foldable foldable )
124 => foldable posting -> balance -> balance
125 postings = flip (Data.Foldable.foldr balance)
127 -- | Return the first given 'Balance'
128 -- updated by the second given 'Balance'.
129 union :: Amount amount
130 => Balance amount -> Balance amount -> Balance amount
133 { balance_by_account = union_by_account
134 (balance_by_account b0)
135 (balance_by_account b1)
136 , balance_by_unit = union_by_unit
141 -- | Return the given 'Balance_by_Account'
142 -- updated by the given 'Posting'.
145 , amount ~ Posting_Amount posting
146 , unit ~ Amount_Unit amount )
148 -> Balance_by_Account amount unit
149 -> Balance_by_Account amount unit
152 (Data.Map.unionWith (flip amount_add))
153 (posting_account post)
154 (posting_amounts post)
156 -- | Return the given 'Balance_by_Unit'
157 -- updated by the given 'Posting'.
160 , amount ~ Posting_Amount posting
161 , unit ~ Amount_Unit amount )
163 -> Balance_by_Unit amount unit
164 -> Balance_by_Unit amount unit
167 (\new old -> Unit_Sum
170 (unit_sum_amount old)
171 (unit_sum_amount new)
172 , unit_sum_accounts =
175 (unit_sum_accounts old)
176 (unit_sum_accounts new)
181 { unit_sum_amount = amount
182 , unit_sum_accounts = Data.Map.singleton (posting_account post) ()
184 (posting_amounts post)
186 -- | Return a 'Balance_by_Unit'
187 -- derived from the given 'Balance_by_Account'.
188 by_unit_of_by_account ::
190 , unit ~ Amount_Unit amount
192 => Balance_by_Account amount unit
193 -> Balance_by_Unit amount unit
194 -> Balance_by_Unit amount unit
195 by_unit_of_by_account =
196 flip $ Lib.TreeMap.foldr_with_Path $ curry by_unit
198 -- | Return the first given 'Balance_by_Account'
199 -- updated by the second given 'Balance_by_Account'.
200 union_by_account :: (Amount amount, unit ~ Amount_Unit amount)
201 => Balance_by_Account amount unit
202 -> Balance_by_Account amount unit
203 -> Balance_by_Account amount unit
206 (Data.Map.unionWith (flip amount_add))
208 -- | Return the first given 'Balance_by_Unit'
209 -- updated by the second given 'Balance_by_Unit'.
210 union_by_unit :: (Amount amount, unit ~ Amount_Unit amount)
211 => Balance_by_Unit amount unit
212 -> Balance_by_Unit amount unit
213 -> Balance_by_Unit amount unit
216 (\new old -> Unit_Sum
217 { unit_sum_amount = amount_add
218 (unit_sum_amount old)
219 (unit_sum_amount new)
220 , unit_sum_accounts = Data.Map.unionWith
222 (unit_sum_accounts old)
223 (unit_sum_accounts new)
226 -- * Type 'Deviation'
228 -- | The 'Balance_by_Unit' whose 'unit_sum_amount'
229 -- is not zero and possible 'Account' to 'infer_equilibrium'.
230 newtype Amount amount
232 = Deviation (Balance_by_Unit amount (Amount_Unit amount))
233 deriving instance ( Amount amount
235 ) => Data (Deviation amount)
236 deriving instance ( Amount amount
238 ) => Eq (Deviation amount)
239 deriving instance ( Amount amount
241 ) => Show (Deviation amount)
242 deriving instance Typeable1 Deviation
243 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
245 -- | Return the 'balance_by_unit' of the given 'Balance' with:
247 -- * 'unit's whose 'unit_sum_amount' verifying 'amount_is_zero' removed,
249 -- * and remaining 'unit's having their 'unit_sum_accounts'
250 -- complemented with the 'balance_by_account' of the given 'Balance'
251 -- (i.e. now mapping to 'Account's __not__ involved to build the 'Unit_Sum').
257 let all_accounts = Lib.TreeMap.flatten (const ()) (balance_by_account bal)
258 let max_accounts = Data.Map.size all_accounts
260 Data.Map.foldlWithKey
261 (\m unit Unit_Sum{unit_sum_amount, unit_sum_accounts} ->
262 if amount_null unit_sum_amount
265 case Data.Map.size unit_sum_accounts of
266 n | n == max_accounts ->
267 Data.Map.insert unit Unit_Sum
269 , unit_sum_accounts = Data.Map.empty
272 let diff = Data.Map.difference all_accounts unit_sum_accounts
273 Data.Map.insert unit Unit_Sum
275 , unit_sum_accounts = diff
279 (balance_by_unit bal)
281 -- ** The equilibrium
283 -- | Return the 'Balance' (adjusted by inferred 'Amount's)
284 -- of the given 'Posting's and either:
286 -- * 'Left': the 'Posting's that cannot be inferred.
287 -- * 'Right': the given 'Posting's with inferred 'Amount's inserted.
290 => Map Account [posting]
291 -> ( Balance (Posting_Amount posting)
292 , Either [Unit_Sum (Posting_Amount posting)] (Map Account [posting])
294 infer_equilibrium posts = do
295 let bal_initial = Data.Foldable.foldr postings nil posts
296 let Deviation dev = deviation bal_initial
297 let (bal_adjusted, eithers) =
298 Data.Map.foldrWithKey
299 (\unit unit_sum@(Unit_Sum{unit_sum_amount, unit_sum_accounts})
301 case Data.Map.size unit_sum_accounts of
303 let acct = fst $ Data.Map.elemAt 0 unit_sum_accounts in
304 let amt = amount_negate unit_sum_amount in
305 let amts = Data.Map.singleton unit amt in
306 ( balance (acct, amts) bal
307 , Right (acct, unit, amt) : lr
309 _ -> (bal, Left [unit_sum] : lr))
312 let (l, r) = Lib.Foldable.accumLeftsAndFoldrRights
313 (\(acct, unit, amt) ->
315 (\_new_ps -> insert_amount (unit, amt))
316 acct (assert False []))
319 [] -> (bal_adjusted, Right r)
320 _ -> (bal_adjusted, Left l)
324 => (Amount_Unit (Posting_Amount posting), Posting_Amount posting)
325 -> [posting] -> [posting]
326 insert_amount p@(unit, amt) ps =
328 [] -> assert False []
329 (x:xs) | Data.Map.null (posting_amounts x) ->
330 posting_set_amounts (Data.Map.singleton unit amt) x:xs
331 | Data.Map.notMember unit (posting_amounts x) ->
332 let amts = Data.Map.insertWith
333 (assert False undefined)
334 unit amt (posting_amounts x) in
335 posting_set_amounts amts x:xs
336 (x:xs) -> x:insert_amount p xs
338 -- | Return 'True' if and only if the given 'Deviation' maps no 'unit'.
339 is_at_equilibrium :: Amount amount => Deviation amount -> Bool
340 is_at_equilibrium (Deviation dev) = Data.Map.null dev
342 -- | Return 'True' if and only if the given 'Deviation'
343 -- maps only to 'Unit_Sum's whose 'unit_sum_accounts'
344 -- maps exactly one 'Account'.
345 is_equilibrium_inferrable :: Amount amount => Deviation amount -> Bool
346 is_equilibrium_inferrable (Deviation dev) =
348 (\Unit_Sum{unit_sum_accounts} -> Data.Map.size unit_sum_accounts == 1)
351 -- | Return 'True' if and only if the given 'Deviation'
352 -- maps to at least one 'Unit_Sum' whose 'unit_sum_accounts'
353 -- maps more than one 'Account'.
354 is_equilibrium_non_inferrable :: Amount amount => Deviation amount -> Bool
355 is_equilibrium_non_inferrable (Deviation dev) =
357 (\Unit_Sum{unit_sum_accounts} -> Data.Map.size unit_sum_accounts > 1)
362 -- | Descending propagation of 'Amount's accross 'Account's.
364 = TreeMap Account.Name (Account_Sum_Expanded amount)
365 data Amount amount => Account_Sum_Expanded amount
366 = Account_Sum_Expanded
367 { exclusive :: Map (Amount_Unit amount) amount
368 , inclusive :: Map (Amount_Unit amount) amount -- ^ 'amount_add' folded over 'exclusive' and 'inclusive' of 'Lib.TreeMap.node_descendants'
370 deriving instance ( Amount amount
372 ) => Data (Account_Sum_Expanded amount)
373 deriving instance ( Amount amount
375 ) => Eq (Account_Sum_Expanded amount)
376 deriving instance ( Amount amount
378 ) => Show (Account_Sum_Expanded amount)
379 deriving instance Typeable1 Account_Sum_Expanded
380 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
382 -- | Return the given 'Balance_by_Account' with:
384 -- * all missing 'Account.ascending' 'Account's inserted,
386 -- * and every mapped 'Amount'
387 -- added with any 'Amount'
388 -- of the 'Account's for which it is 'Account.ascending'.
391 => Balance_by_Account amount (Amount_Unit amount)
394 let from_value = fromMaybe (assert False undefined) . Lib.TreeMap.node_value in
395 Lib.TreeMap.map_by_depth_first
396 (\descendants value ->
397 let nodes = Lib.TreeMap.nodes descendants in
398 let exclusive = fromMaybe Data.Map.empty value in
403 (Data.Map.unionWith amount_add . inclusive . from_value)
407 -- | Return a 'Balance_by_Unit'
408 -- derived from the given 'Expanded' balance.
410 -- NOTE: also correct if the 'Expanded' has been filtered.
411 by_unit_of_expanded ::
413 , unit ~ Amount_Unit amount
416 -> Balance_by_Unit amount unit
417 -> Balance_by_Unit amount unit
418 by_unit_of_expanded =
421 go p (Lib.TreeMap.TreeMap m) bal =
422 Data.Map.foldrWithKey
423 (\k Lib.TreeMap.Node{Lib.TreeMap.node_value, Lib.TreeMap.node_descendants} acc ->
425 Nothing -> go (k:p) node_descendants acc
427 let account = Lib.TreeMap.reverse $ Lib.TreeMap.path k p in
428 by_unit (account, inclusive a) acc)