1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE FlexibleContexts #-}
3 {-# LANGUAGE FlexibleInstances #-}
4 {-# LANGUAGE NamedFieldPuns #-}
5 {-# LANGUAGE StandaloneDeriving #-}
6 {-# LANGUAGE TypeFamilies #-}
7 module Hcompta.Calc.Balance where
9 import Control.Exception (assert)
11 import qualified Data.Foldable
12 import Data.Foldable (Foldable(..))
13 import qualified Data.Map.Strict as Data.Map
14 import Data.Map.Strict (Map)
15 import Data.Maybe (fromMaybe)
16 import Data.Typeable ()
18 import qualified Hcompta.Lib.Foldable as Lib.Foldable
19 import qualified Hcompta.Lib.TreeMap as Lib.TreeMap
20 import Hcompta.Lib.TreeMap (TreeMap)
21 import qualified Hcompta.Model.Account as Account
22 import Hcompta.Model.Account (Account)
24 -- * Requirements' interface
28 ( Data (Amount_Unit a)
32 , Show (Amount_Unit a)
34 , Typeable (Amount_Unit a)
38 amount_null :: a -> Bool
39 amount_add :: a -> a -> a
40 amount_negate :: a -> a
41 amount_positive :: a -> Maybe a
42 amount_negative :: a -> Maybe a
44 instance (Amount a, unit ~ Amount_Unit a)
45 => Amount (Map unit a) where
46 type Amount_Unit (Map unit a) = Amount_Unit a
47 amount_null = Data.Foldable.all amount_null
48 amount_add = Data.Map.unionWith amount_add
49 amount_negate = Data.Map.map amount_negate
51 let m = Data.Map.mapMaybe amount_negative a in
56 let m = Data.Map.mapMaybe amount_positive a in
63 -- | A 'posting' used to produce a 'Balance'
64 -- must be an instance of this class.
65 class Amount (Posting_Amount p) => Posting p where
67 posting_account :: p -> Account
68 posting_amounts :: p -> Map (Amount_Unit (Posting_Amount p)) (Posting_Amount p)
69 posting_set_amounts :: Map (Amount_Unit (Posting_Amount p)) (Posting_Amount p) -> p -> p
71 instance (Amount amount, unit ~ Amount_Unit amount)
72 => Posting (Account, Map unit amount)
74 type Posting_Amount (Account, Map unit amount) = amount
77 posting_set_amounts amounts (acct, _) = (acct, amounts)
81 -- | Sum by 'Account' and sum by 'unit' of some 'Posting's.
82 data Amount amount => Balance amount
84 { balance_by_account :: Balance_by_Account amount (Amount_Unit amount)
85 , balance_by_unit :: Balance_by_Unit amount (Amount_Unit amount)
87 deriving instance Amount amount => Data (Balance amount)
88 deriving instance Amount amount => Eq (Balance amount)
89 deriving instance Amount amount => Show (Balance amount)
90 deriving instance Typeable Balance
92 type Balance_by_Account amount unit
93 = TreeMap Account.Name
94 (Account_Sum amount unit)
96 -- | A sum of 'amount's,
97 -- concerning a single 'Account'.
98 type Account_Sum amount unit
99 = Data.Map.Map unit amount
101 type Balance_by_Unit amount unit
102 = Map unit (Unit_Sum amount)
104 -- | A sum of 'amount's with their 'Account's involved,
105 -- concerning a single 'unit'.
108 { unit_sum_amount :: amount -- ^ The sum of 'amount's for a single 'unit'.
109 , unit_sum_accounts :: Map Account () -- ^ The 'Account's involved to build 'unit_sum_amount'.
110 } deriving (Data, Eq, Show, Typeable)
114 nil :: Amount amount => Balance amount
117 { balance_by_account = Lib.TreeMap.empty
118 , balance_by_unit = Data.Map.empty
121 -- | Return the given 'Balance_by_Account'
122 -- updated by the given 'Posting'.
125 , amount ~ Posting_Amount posting
126 , unit ~ Amount_Unit amount )
128 -> Balance_by_Account amount unit
129 -> Balance_by_Account amount unit
132 (Data.Map.unionWith (flip amount_add))
133 (posting_account post)
134 (posting_amounts post)
136 -- | Return the given 'Balance_by_Unit'
137 -- updated by the given 'Posting'.
140 , amount ~ Posting_Amount posting
141 , unit ~ Amount_Unit amount )
143 -> Balance_by_Unit amount unit
144 -> Balance_by_Unit amount unit
147 (\new old -> Unit_Sum
150 (unit_sum_amount old)
151 (unit_sum_amount new)
152 , unit_sum_accounts =
155 (unit_sum_accounts old)
156 (unit_sum_accounts new)
161 { unit_sum_amount = amount
162 , unit_sum_accounts = Data.Map.singleton (posting_account post) ()
164 (posting_amounts post)
166 -- | Return a 'Balance_by_Unit'
167 -- derived from the given 'Balance_by_Account'.
168 by_unit_of_by_account ::
170 , unit ~ Amount_Unit amount
172 => Balance_by_Account amount unit
173 -> Balance_by_Unit amount unit
174 -> Balance_by_Unit amount unit
175 by_unit_of_by_account =
176 flip $ Lib.TreeMap.foldr_with_Path $ curry by_unit
178 -- | Return the given 'Balance'
179 -- updated by the given 'Posting'.
182 , balance ~ Balance (Posting_Amount posting) )
183 => posting -> balance -> balance
186 { balance_by_account = by_account post (balance_by_account bal)
187 , balance_by_unit = by_unit post (balance_by_unit bal)
190 -- | Return the given 'Balance'
191 -- updated by the given 'Posting's.
194 , balance ~ Balance (Posting_Amount posting)
195 , Foldable foldable )
196 => foldable posting -> balance -> balance
197 postings = flip (Data.Foldable.foldr balance)
199 -- | Return the first given 'Balance'
200 -- updated by the second given 'Balance'.
201 union :: Amount amount
202 => Balance amount -> Balance amount -> Balance amount
205 { balance_by_account =
207 (Data.Map.unionWith (flip amount_add))
208 (balance_by_account b0)
209 (balance_by_account b1)
212 (\new old -> Unit_Sum
213 { unit_sum_amount = amount_add
214 (unit_sum_amount old)
215 (unit_sum_amount new)
216 , unit_sum_accounts = Data.Map.unionWith
218 (unit_sum_accounts old)
219 (unit_sum_accounts new)
225 -- * Type 'Deviation'
227 -- | The 'Balance_by_Unit' whose 'unit_sum_amount'
228 -- is not zero and possible 'Account' to 'infer_equilibrium'.
229 newtype Amount amount
231 = Deviation (Balance_by_Unit amount (Amount_Unit amount))
232 deriving instance Amount amount => Data (Deviation amount)
233 deriving instance Amount amount => Eq (Deviation amount)
234 deriving instance Amount amount => Show (Deviation amount)
235 deriving instance Typeable Deviation
237 -- | Return the 'balance_by_unit' of the given 'Balance' with:
239 -- * 'unit's whose 'unit_sum_amount' verifying 'amount_is_zero' removed,
241 -- * and remaining 'unit's having their 'unit_sum_accounts'
242 -- complemented with the 'balance_by_account' of the given 'Balance'
243 -- (i.e. now mapping to 'Account's __not__ involved to build the 'Unit_Sum').
249 let all_accounts = Lib.TreeMap.flatten (const ()) (balance_by_account bal)
250 let max_accounts = Data.Map.size all_accounts
252 Data.Map.foldlWithKey
253 (\m unit Unit_Sum{unit_sum_amount, unit_sum_accounts} ->
254 if amount_null unit_sum_amount
257 case Data.Map.size unit_sum_accounts of
258 n | n == max_accounts ->
259 Data.Map.insert unit Unit_Sum
261 , unit_sum_accounts = Data.Map.empty
264 let diff = Data.Map.difference all_accounts unit_sum_accounts
265 Data.Map.insert unit Unit_Sum
267 , unit_sum_accounts = diff
271 (balance_by_unit bal)
273 -- ** The equilibrium
275 -- | Return the 'Balance' (adjusted by inferred 'Amount's)
276 -- of the given 'Posting's and either:
278 -- * 'Left': the 'Posting's that cannot be inferred.
279 -- * 'Right': the given 'Posting's with inferred 'Amount's inserted.
282 => Map Account [posting]
283 -> ( Balance (Posting_Amount posting)
284 , Either [Unit_Sum (Posting_Amount posting)] (Map Account [posting])
286 infer_equilibrium posts = do
287 let bal_initial = Data.Foldable.foldr postings nil posts
288 let Deviation dev = deviation bal_initial
289 let (bal_adjusted, eithers) =
290 Data.Map.foldrWithKey
291 (\unit unit_sum@(Unit_Sum{unit_sum_amount, unit_sum_accounts})
293 case Data.Map.size unit_sum_accounts of
295 let acct = fst $ Data.Map.elemAt 0 unit_sum_accounts in
296 let amt = amount_negate unit_sum_amount in
297 let amts = Data.Map.singleton unit amt in
298 ( balance (acct, amts) bal
299 , Right (acct, unit, amt) : lr
301 _ -> (bal, Left [unit_sum] : lr))
304 let (l, r) = Lib.Foldable.accumLeftsAndFoldrRights
305 (\(acct, unit, amt) ->
307 (\_new_ps -> insert_amount (unit, amt))
308 acct (assert False []))
311 [] -> (bal_adjusted, Right r)
312 _ -> (bal_adjusted, Left l)
316 => (Amount_Unit (Posting_Amount posting), Posting_Amount posting)
317 -> [posting] -> [posting]
318 insert_amount p@(unit, amt) ps =
320 [] -> assert False []
321 (x:xs) | Data.Map.null (posting_amounts x) ->
322 posting_set_amounts (Data.Map.singleton unit amt) x:xs
323 | Data.Map.notMember unit (posting_amounts x) ->
324 let amts = Data.Map.insertWith
325 (assert False undefined)
326 unit amt (posting_amounts x) in
327 posting_set_amounts amts x:xs
328 (x:xs) -> x:insert_amount p xs
330 -- | Return 'True' if and only if the given 'Deviation' maps no 'unit'.
331 is_at_equilibrium :: Amount amount => Deviation amount -> Bool
332 is_at_equilibrium (Deviation dev) = Data.Map.null dev
334 -- | Return 'True' if and only if the given 'Deviation'
335 -- maps only to 'Unit_Sum's whose 'unit_sum_accounts'
336 -- maps exactly one 'Account'.
337 is_equilibrium_inferrable :: Amount amount => Deviation amount -> Bool
338 is_equilibrium_inferrable (Deviation dev) =
340 (\Unit_Sum{unit_sum_accounts} -> Data.Map.size unit_sum_accounts == 1)
343 -- | Return 'True' if and only if the given 'Deviation'
344 -- maps to at least one 'Unit_Sum' whose 'unit_sum_accounts'
345 -- maps more than one 'Account'.
346 is_equilibrium_non_inferrable :: Amount amount => Deviation amount -> Bool
347 is_equilibrium_non_inferrable (Deviation dev) =
349 (\Unit_Sum{unit_sum_accounts} -> Data.Map.size unit_sum_accounts > 1)
354 -- | Descending propagation of 'Amount's accross 'Account's.
356 = TreeMap Account.Name (Account_Sum_Expanded amount)
357 data Amount amount => Account_Sum_Expanded amount
358 = Account_Sum_Expanded
359 { exclusive :: Map (Amount_Unit amount) amount
360 , inclusive :: Map (Amount_Unit amount) amount -- ^ 'amount_add' folded over 'exclusive' and 'inclusive' of 'Lib.TreeMap.node_descendants'
362 deriving instance Amount amount => Data (Account_Sum_Expanded amount)
363 deriving instance Amount amount => Eq (Account_Sum_Expanded amount)
364 deriving instance Amount amount => Show (Account_Sum_Expanded amount)
365 deriving instance Typeable Account_Sum_Expanded
367 -- | Return the given 'Balance_by_Account' with:
369 -- * all missing 'Account.ascending' 'Account's inserted,
371 -- * and every mapped 'Amount'
372 -- added with any 'Amount'
373 -- of the 'Account's for which it is 'Account.ascending'.
376 => Balance_by_Account amount (Amount_Unit amount)
379 let from_value = fromMaybe (assert False undefined) . Lib.TreeMap.node_value in
380 Lib.TreeMap.map_by_depth_first
381 (\descendants value ->
382 let nodes = Lib.TreeMap.nodes descendants in
383 let exclusive = fromMaybe Data.Map.empty value in
388 (Data.Map.unionWith amount_add . inclusive . from_value)
392 -- | Return a 'Balance_by_Unit'
393 -- derived from the given 'Expanded' balance.
395 -- NOTE: also correct if the 'Expanded' has been filtered.
396 by_unit_of_expanded ::
398 , unit ~ Amount_Unit amount
401 -> Balance_by_Unit amount unit
402 -> Balance_by_Unit amount unit
403 by_unit_of_expanded =
406 go p (Lib.TreeMap.TreeMap m) bal =
407 Data.Map.foldrWithKey
408 (\k Lib.TreeMap.Node{Lib.TreeMap.node_value, Lib.TreeMap.node_descendants} acc ->
410 Nothing -> go (k:p) node_descendants acc
412 let account = Lib.TreeMap.reverse $ Lib.TreeMap.path k p in
413 by_unit (account, inclusive a) acc)
416 -- * Type 'Amount_Sum'
418 -- | Sum keeping track of negative and positive 'Amount's.
422 { amount_sum_negative :: Maybe amount
423 , amount_sum_positive :: Maybe amount
424 , amount_sum_balance :: amount
425 } deriving (Data, Eq, Show, Typeable)
427 instance Amount a => Amount (Amount_Sum a) where
428 type Amount_Unit (Amount_Sum a) = Amount_Unit a
429 amount_null = amount_null . amount_sum_balance
432 case (get a0, get a1) of
435 (Just x0, Just x1) -> Just $ amount_add x0 x1 in
437 { amount_sum_negative = add amount_sum_negative
438 , amount_sum_positive = add amount_sum_positive
439 , amount_sum_balance = amount_add (amount_sum_balance a0) (amount_sum_balance a1)
443 { amount_sum_negative = amount_sum_positive a
444 , amount_sum_positive = amount_sum_negative a
445 , amount_sum_balance = amount_negate $ amount_sum_balance a
449 (\amt -> Just $ Amount_Sum
450 { amount_sum_negative = Just amt
451 , amount_sum_positive = Nothing
452 , amount_sum_balance = amt
454 (amount_sum_negative a)
457 (\amt -> Just $ Amount_Sum
458 { amount_sum_negative = Nothing
459 , amount_sum_positive = Just amt
460 , amount_sum_balance = amt
462 (amount_sum_positive a)
466 => amount -> Amount_Sum amount
469 { amount_sum_negative = amount_negative a
470 , amount_sum_positive = amount_positive a
471 , amount_sum_balance = a