]> Git — Sourcephile - comptalang.git/blob - lib/Hcompta/Balance.hs
Modification : filtre dès la lecture pour moins de consommation mémoire.
[comptalang.git] / lib / Hcompta / Balance.hs
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
11
12 import Control.Applicative (Const(..))
13 import Control.Exception (assert)
14 import Data.Data
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 ()
21
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)
28
29 -- * Requirements' interface
30
31 -- ** Class 'Amount'
32 class
33 ( Data (Amount_Unit a)
34 , Ord (Amount_Unit a)
35 , Show (Amount_Unit a)
36 , Typeable (Amount_Unit a)
37 ) => Amount a where
38 type Amount_Unit a
39 amount_null :: a -> Bool
40 amount_add :: a -> a -> a
41 amount_negate :: a -> a
42
43 -- ** Class 'Posting'
44
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
48 type Posting_Amount p
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
52
53 {-
54 instance (Amount amount, unit ~ Amount_Unit amount)
55 => Posting (Account, Map unit amount)
56 where
57 type Posting_Amount (Account, Map unit amount) = amount
58 posting_account = fst
59 posting_amounts = snd
60 posting_set_amounts amounts (acct, _) = (acct, amounts)
61 -}
62
63 instance (Amount amount)
64 => Posting (Account, Account_Sum amount)
65 where
66 type Posting_Amount (Account, Account_Sum amount) = amount
67 posting_account = fst
68 posting_amounts (_, Account_Sum x) = x
69 posting_set_amounts amounts (acct, _) = (acct, Account_Sum amounts)
70
71 -- * Type 'Balance'
72
73 -- | Sum by 'Account' and sum by 'unit' of some 'Posting's.
74 data Amount amount
75 => Balance amount
76 = Balance
77 { balance_by_account :: !(Balance_by_Account amount)
78 , balance_by_unit :: !(Balance_by_Unit amount)
79 }
80 deriving instance ( Amount amount
81 , Data amount
82 ) => Data (Balance amount)
83 deriving instance ( Amount amount
84 , Eq amount
85 ) => Eq (Balance amount)
86 deriving instance ( Amount amount
87 , Show amount
88 ) => Show (Balance amount)
89 deriving instance Typeable1 Balance
90 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
91
92 instance Amount amount => Monoid (Balance amount) where
93 mempty = empty
94 mappend = union
95
96 -- ** Type 'Balance_by_Account'
97 type Balance_by_Account amount
98 = TreeMap Account.Name
99 (Account_Sum amount)
100
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
110 , Data amount
111 ) => Data (Account_Sum amount)
112 deriving instance ( Amount amount
113 , Eq amount
114 ) => Eq (Account_Sum amount)
115 deriving instance ( Amount amount
116 , Show amount
117 ) => Show (Account_Sum amount)
118 deriving instance Typeable1 Account_Sum
119 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
120
121 instance Amount amount
122 => Monoid (Account_Sum amount) where
123 mempty = Account_Sum mempty
124 mappend
125 (Account_Sum a0)
126 (Account_Sum a1) =
127 Account_Sum $ Data.Map.unionWith amount_add a0 a1
128
129 {- NOTE: overlapping with the instance below.
130 instance
131 ( Posting posting
132 , amount ~ Posting_Amount posting
133 )
134 => Consable (Const (Balance_by_Account amount)) posting where
135 mcons p = Const . by_account p . getConst
136 -}
137 instance
138 ( Foldable foldable
139 , Posting posting
140 , amount ~ Posting_Amount posting
141 )
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
146
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
152 , Data amount
153 ) => Data (Balance_by_Unit amount)
154 deriving instance ( Amount amount
155 , Eq amount
156 ) => Eq (Balance_by_Unit amount)
157 deriving instance ( Amount amount
158 , Show amount
159 ) => Show (Balance_by_Unit amount)
160 deriving instance Typeable1 Balance_by_Unit
161 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
162
163 instance Amount amount
164 => Monoid (Balance_by_Unit amount) where
165 mempty = Balance_by_Unit mempty
166 mappend = union_by_unit
167
168 -- *** Type 'Unit_Sum'
169
170 -- | A sum of 'amount's with their 'Account's involved,
171 -- concerning a single 'unit'.
172 data Unit_Sum amount
173 = Unit_Sum
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)
177
178 -- ** Constructors
179
180 empty :: Amount amount => Balance amount
181 empty =
182 Balance
183 { balance_by_account = mempty
184 , balance_by_unit = mempty
185 }
186
187 -- | Return the given 'Balance'
188 -- updated by the given 'Posting'.
189 balance ::
190 ( Posting posting
191 , balance ~ Balance (Posting_Amount posting) )
192 => posting -> balance -> balance
193 balance post bal =
194 bal
195 { balance_by_account = by_account post (balance_by_account bal)
196 , balance_by_unit = by_unit post (balance_by_unit bal)
197 }
198
199 -- | Return the given 'Balance'
200 -- updated by the given 'Posting's.
201 postings ::
202 ( Posting posting
203 , balance ~ Balance (Posting_Amount posting)
204 , Foldable foldable )
205 => foldable posting -> balance -> balance
206 postings = flip (Data.Foldable.foldr balance)
207
208 -- | Return the first given 'Balance'
209 -- updated by the second given 'Balance'.
210 union :: Amount amount
211 => Balance amount -> Balance amount -> Balance amount
212 union
213 (Balance b0a b0u)
214 (Balance b1a b1u) =
215 Balance
216 { balance_by_account = union_by_account b0a b1a
217 , balance_by_unit = union_by_unit b0u b1u
218 }
219
220 -- | Return the given 'Balance_by_Account'
221 -- updated by the given 'Posting'.
222 by_account ::
223 ( Posting posting
224 , amount ~ Posting_Amount posting
225 , unit ~ Amount_Unit amount
226 )
227 => posting
228 -> Balance_by_Account amount
229 -> Balance_by_Account amount
230 by_account post =
231 Lib.TreeMap.insert mappend
232 (posting_account post)
233 (Account_Sum $ posting_amounts post)
234
235 -- | Return the given 'Balance_by_Unit'
236 -- updated by the given 'Posting'.
237 by_unit ::
238 ( Posting posting
239 , amount ~ Posting_Amount posting
240 , unit ~ Amount_Unit amount )
241 => posting
242 -> Balance_by_Unit amount
243 -> Balance_by_Unit amount
244 by_unit post =
245 union_by_unit $
246 Balance_by_Unit $
247 Data.Map.map
248 (\amount -> Unit_Sum
249 { unit_sum_amount = amount
250 , unit_sum_accounts = Data.Map.singleton (posting_account post) ()
251 })
252 (posting_amounts post)
253
254 -- | Return a 'Balance_by_Unit'
255 -- derived from the given 'Balance_by_Account'.
256 by_unit_of_by_account ::
257 ( Amount amount
258 , unit ~ Amount_Unit amount
259 )
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
265
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
273
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
280 union_by_unit
281 (Balance_by_Unit a0)
282 (Balance_by_Unit a1) =
283 Balance_by_Unit $
284 Data.Map.unionWith
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
290 (const::()->()->())
291 (unit_sum_accounts old)
292 (unit_sum_accounts new)
293 })
294 a0 a1
295
296 -- * Type 'Deviation'
297
298 -- | The 'Balance_by_Unit' whose 'unit_sum_amount'
299 -- is not zero and possible 'Account' to 'infer_equilibrium'.
300 newtype Amount amount
301 => Deviation amount
302 = Deviation (Balance_by_Unit amount)
303 deriving instance ( Amount amount
304 , Data amount
305 ) => Data (Deviation amount)
306 deriving instance ( Amount amount
307 , Eq amount
308 ) => Eq (Deviation amount)
309 deriving instance ( Amount amount
310 , Show amount
311 ) => Show (Deviation amount)
312 deriving instance Typeable1 Deviation
313 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
314
315 -- | Return the 'balance_by_unit' of the given 'Balance' with:
316 --
317 -- * 'unit's whose 'unit_sum_amount' verifying 'amount_is_zero' removed,
318 --
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').
322 deviation
323 :: Amount amount
324 => Balance amount
325 -> Deviation amount
326 deviation Balance
327 { balance_by_account=ba
328 , balance_by_unit=Balance_by_Unit bu
329 } = do
330 let all_accounts = Lib.TreeMap.flatten (const ()) ba
331 let max_accounts = Data.Map.size all_accounts
332 Deviation $
333 Data.Map.foldlWithKey
334 (\(Balance_by_Unit m) unit Unit_Sum{unit_sum_amount, unit_sum_accounts} ->
335 Balance_by_Unit $
336 if amount_null unit_sum_amount
337 then m
338 else
339 case Data.Map.size unit_sum_accounts of
340 n | n == max_accounts ->
341 Data.Map.insert unit Unit_Sum
342 { unit_sum_amount
343 , unit_sum_accounts = Data.Map.empty
344 } m
345 _ -> do
346 let diff = Data.Map.difference all_accounts unit_sum_accounts
347 Data.Map.insert unit Unit_Sum
348 { unit_sum_amount
349 , unit_sum_accounts = diff
350 } m
351 )
352 mempty
353 bu
354
355 -- ** The equilibrium
356
357 -- | Return the 'Balance' (adjusted by inferred 'Amount's)
358 -- of the given 'Posting's and either:
359 --
360 -- * 'Left': the 'Posting's that cannot be inferred.
361 -- * 'Right': the given 'Posting's with inferred 'Amount's inserted.
362 infer_equilibrium ::
363 ( Posting posting )
364 => Map Account [posting]
365 -> ( Balance (Posting_Amount posting)
366 , Either [Unit_Sum (Posting_Amount posting)] (Map Account [posting])
367 )
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})
374 (bal, lr) ->
375 case Data.Map.size unit_sum_accounts of
376 1 ->
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
382 )
383 _ -> (bal, Left [unit_sum] : lr))
384 (bal_initial, [])
385 dev
386 let (l, r) = Lib.Foldable.accumLeftsAndFoldrRights
387 (\(acct, unit, amt) ->
388 Data.Map.insertWith
389 (\_new_ps -> insert_amount (unit, amt))
390 acct (assert False []))
391 posts eithers
392 case l of
393 [] -> (bal_adjusted, Right r)
394 _ -> (bal_adjusted, Left l)
395 where
396 insert_amount
397 :: Posting posting
398 => (Amount_Unit (Posting_Amount posting), Posting_Amount posting)
399 -> [posting] -> [posting]
400 insert_amount p@(unit, amt) ps =
401 case ps of
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
411
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
415
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)) =
421 Data.Foldable.all
422 (\Unit_Sum{unit_sum_accounts} -> Data.Map.size unit_sum_accounts == 1)
423 dev
424
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)) =
430 Data.Foldable.any
431 (\Unit_Sum{unit_sum_accounts} -> Data.Map.size unit_sum_accounts > 1)
432 dev
433
434 -- * Type 'Expanded'
435
436 -- | Descending propagation of 'Amount's accross 'Account's.
437 type Expanded amount
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'
443 }
444 deriving instance ( Amount amount
445 , Data amount
446 ) => Data (Account_Sum_Expanded amount)
447 deriving instance ( Amount amount
448 , Eq amount
449 ) => Eq (Account_Sum_Expanded amount)
450 deriving instance ( Amount amount
451 , Show amount
452 ) => Show (Account_Sum_Expanded amount)
453 deriving instance Typeable1 Account_Sum_Expanded
454 -- FIXME: use 'Typeable' when dropping GHC-7.6 support
455
456 instance Amount amount => Monoid (Account_Sum_Expanded amount) where
457 mempty = Account_Sum_Expanded mempty mempty
458 mappend
459 (Account_Sum_Expanded e0 i0)
460 (Account_Sum_Expanded e1 i1) =
461 Account_Sum_Expanded
462 (mappend e0 e1)
463 (mappend i0 i1)
464
465 -- | Return the given 'Balance_by_Account' with:
466 --
467 -- * all missing 'Account.ascending' 'Account's inserted,
468 --
469 -- * and every mapped 'Amount'
470 -- added with any 'Amount'
471 -- of the 'Account's for which it is 'Account.ascending'.
472 expanded
473 :: Amount amount
474 => Balance_by_Account amount
475 -> Expanded amount
476 expanded =
477 Lib.TreeMap.map_by_depth_first
478 (\descendants value ->
479 let exclusive = Strict.fromMaybe mempty value in
480 Account_Sum_Expanded
481 { exclusive
482 , inclusive =
483 Data.Map.foldl'
484 ( flip $ mappend . inclusive
485 . Strict.fromMaybe (assert False undefined)
486 . Lib.TreeMap.node_value)
487 exclusive $
488 Lib.TreeMap.nodes descendants
489 })
490
491 -- | Return a 'Balance_by_Unit'
492 -- derived from the given 'Expanded' balance.
493 --
494 -- NOTE: also correct if the 'Expanded' has been filtered.
495 by_unit_of_expanded ::
496 ( Amount amount
497 , unit ~ Amount_Unit amount
498 )
499 => Expanded amount
500 -> Balance_by_Unit amount
501 -> Balance_by_Unit amount
502 by_unit_of_expanded =
503 go []
504 where
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 ->
508 case node_value of
509 Strict.Nothing -> go (k:p) node_descendants acc
510 Strict.Just a ->
511 let account = Lib.TreeMap.reverse $ Lib.TreeMap.path k p in
512 by_unit (account, inclusive a) acc)
513 bal m