1 {-# OPTIONS_GHC -fno-warn-orphans #-}
2 module Hcompta.Balance where
4 import Control.DeepSeq (NFData(..))
5 import Control.Exception (assert)
8 import Data.Either (Either(..))
9 import Data.Eq (Eq(..))
10 import Data.Function (($), (.), const, flip)
11 import Data.Map.Strict (Map)
12 import Data.Monoid (Monoid(..))
13 import Data.NonNull (NonNull(..))
14 import Data.Ord (Ord(..))
15 import Data.Semigroup (Semigroup(..))
16 import Data.TreeMap.Strict (TreeMap(..))
17 import Data.Tuple (curry, fst, snd)
18 import Data.Typeable ()
19 import Prelude (seq, undefined)
20 import Text.Show (Show(..))
21 import qualified Data.Foldable as Foldable
22 import qualified Data.Map.Strict as Map
23 import qualified Data.MonoTraversable as MT
24 import qualified Data.Sequences as Seqs
25 import qualified Data.Strict.Maybe as Strict
26 import qualified Data.TreeMap.Strict as TreeMap
29 import qualified Hcompta.Lib.Foldable as Foldable
30 import qualified Hcompta.Lib.Strict as Strict
31 import Hcompta.Quantity
33 -- * Type 'Balance_Account'
34 -- | 'Balance' operations works on this type of 'Account'.
35 type Balance_Account a = TreeMap.Path a
36 instance Get (Balance_Account acct_sect)
37 (Balance_Account acct_sect
38 ,Balance_Amounts unit qty) where get = fst
40 -- * Type 'Balance_Amounts'
41 -- | 'Balance' operations works on this type of 'Amounts'.
42 type Balance_Amounts = Map
43 instance Get (Balance_Amounts unit qty)
44 (Balance_Account acct_sect
45 ,Balance_Amounts unit qty) where get = snd
46 instance Set (Balance_Amounts unit qty)
47 (Balance_Account acct_sect
48 ,Balance_Amounts unit qty) where set x (a, _) = (a, x)
52 -- | 'Balance_Account' and 'BalByUnit' of some @post@s.
54 -- NOTE: to reduce memory consumption
55 -- when applying 'balance_cons' incrementally,
56 -- the fields are explicitely stricts.
57 data Balance acct_sect unit qty
59 { balByAccount :: !(BalByAccount acct_sect unit qty)
60 , balByUnit :: !(BalByUnit acct_sect unit qty)
61 } deriving (Data, Eq, Show, Typeable)
67 ) => NFData (Balance acct_sect unit qty) where
68 rnf (Balance a u) = rnf a `seq` rnf u
73 ) => Semigroup (Balance acct_sect unit qty) where
79 ) => Monoid (Balance acct_sect unit qty) where
80 mempty = balance_empty
83 balance_empty :: Balance acct_sect unit qty
86 { balByAccount = TreeMap.empty
87 , balByUnit = BalByUnit Map.empty
90 -- | Return the first given 'Balance'
91 -- updated by the second given 'Balance'.
93 (Addable qty, Ord acct_sect, Ord unit)
94 => Balance acct_sect unit qty
95 -> Balance acct_sect unit qty
96 -> Balance acct_sect unit qty
101 { balByAccount = balByAccount_union b0a b1a
102 , balByUnit = balByUnit_union b0u b1u
105 -- | Return the given 'Balance'
106 -- updated by the given @post@.
108 ( Get (Balance_Account acct_sect) post
109 , Get (Balance_Amounts unit qty) post
110 , Addable qty, Ord acct_sect, Ord unit )
112 -> Balance acct_sect unit qty
113 -> Balance acct_sect unit qty
114 balance_cons post bal =
116 { balByAccount = balByAccount_cons post (balByAccount bal)
117 , balByUnit = balByUnit_cons post (balByUnit bal)
120 -- | Return the given 'Balance'
121 -- updated by the given @post@s.
123 ( post ~ MT.Element posts
124 , MT.MonoFoldable posts
125 , Get (Balance_Account acct_sect) post
126 , Get (Balance_Amounts unit qty) post
127 , Addable qty, Ord acct_sect, Ord unit )
129 -> Balance acct_sect unit qty
130 -> Balance acct_sect unit qty
131 balance_postings = flip (MT.ofoldr balance_cons)
133 -- ** Type 'BalByAccount'
134 type BalByAccount acct_sect unit qty
135 = TreeMap acct_sect (SumByAccount unit qty)
137 -- | Return the first given 'BalByAccount'
138 -- updated by the second given 'BalByAccount'.
139 balByAccount_union ::
143 => BalByAccount acct_sect unit qty
144 -> BalByAccount acct_sect unit qty
145 -> BalByAccount acct_sect unit qty
146 balByAccount_union = TreeMap.union (<>)
148 -- | Return the given 'BalByAccount'
149 -- updated by the given @post@.
151 ( Get (Balance_Account acct_sect) post
152 , Get (Balance_Amounts unit qty) post
157 -> BalByAccount acct_sect unit qty
158 -> BalByAccount acct_sect unit qty
159 balByAccount_cons post =
160 TreeMap.insert (<>) (get post) (SumByAccount $ get post)
162 -- *** Type 'SumByAccount'
163 -- | A sum of @qty@s, concerning a single 'Balance_Account'.
164 newtype SumByAccount unit qty
165 = SumByAccount (Balance_Amounts unit qty)
166 deriving (Data, Eq, NFData, Show, Typeable)
167 instance -- Semigroup
168 (Addable qty, Ord unit) =>
169 Semigroup (SumByAccount unit qty) where
170 SumByAccount x <> SumByAccount y =
171 SumByAccount $ Map.unionWith (flip quantity_add) x y
173 (Addable qty, Ord unit) =>
174 Monoid (SumByAccount unit qty) where
175 mempty = SumByAccount mempty
179 :: SumByAccount unit qty
181 unSumByAccount (SumByAccount m) = m
183 -- ** Type 'BalByUnit'
184 newtype BalByUnit acct_sect unit qty
185 = BalByUnit (Map unit (SumByUnit (Balance_Account acct_sect) qty))
186 deriving (Data, Eq, NFData, Show, Typeable)
187 instance -- Semigroup
188 (Addable qty, Ord acct_sect, Ord unit) =>
189 Semigroup (BalByUnit acct_sect unit qty) where
190 (<>) = balByUnit_union
192 (Addable qty, Ord acct_sect, Ord unit) =>
193 Monoid (BalByUnit acct_sect unit qty) where
194 mempty = BalByUnit mempty
197 -- | Return the first given 'BalByUnit'
198 -- updated by the second given 'BalByUnit'.
200 :: (Addable qty, Ord acct_sect, Ord unit)
201 => BalByUnit acct_sect unit qty
202 -> BalByUnit acct_sect unit qty
203 -> BalByUnit acct_sect unit qty
209 (\new old -> SumByUnit
210 { sumByUnit_quantity = quantity_add
211 (sumByUnit_quantity old)
212 (sumByUnit_quantity new)
213 , sumByUnit_accounts = Map.unionWith
215 (sumByUnit_accounts old)
216 (sumByUnit_accounts new)
220 -- | Return the given 'BalByUnit'
221 -- updated by the given @post@.
223 ( Get (Balance_Account acct_sect) post
224 , Get (Balance_Amounts unit qty) post
229 -> BalByUnit acct_sect unit qty
230 -> BalByUnit acct_sect unit qty
231 balByUnit_cons post =
234 (`Map.map` get post) $
236 { sumByUnit_quantity = qty
237 , sumByUnit_accounts = Map.singleton (get post) ()
240 -- | Return the given 'BalByUnit'
241 -- updated by the given 'BalByAccount'.
242 balByUnit_of_BalByAccount
243 :: (Addable qty, Ord acct_sect, Ord unit)
244 => BalByAccount acct_sect unit qty
245 -> BalByUnit acct_sect unit qty
246 -> BalByUnit acct_sect unit qty
247 balByUnit_of_BalByAccount =
248 flip $ TreeMap.foldr_with_Path $ curry balByUnit_cons
250 instance Get ( Balance_Account acct_sect )
251 ( Balance_Account acct_sect
252 , SumByAccount unit qty )
254 instance Get ( Balance_Amounts unit qty )
255 ( Balance_Account acct_sect
256 , SumByAccount unit qty )
257 where get = unSumByAccount . snd
259 -- *** Type 'SumByUnit'
260 -- | A sum of @qty@s with their 'Account's involved,
261 -- concerning a single @unit@.
262 data SumByUnit acct qty
264 { sumByUnit_quantity :: !qty
265 -- ^ The sum of @qty@s for a single @unit@.
266 , sumByUnit_accounts :: !(Map acct ())
267 -- ^ The 'Balance_Account's involved to build 'sumByUnit_quantity'.
268 } deriving (Data, Eq, Ord, Show, Typeable)
272 ) => NFData (SumByUnit acct qty) where
273 rnf (SumByUnit q a) = rnf q `seq` rnf a
275 -- * Type 'DeviationByUnit'
276 -- | The 'BalByUnit' whose 'sumByUnit_quantity'
277 -- is not zero and possible 'Balance_Account' to 'equilibrium'.
278 newtype DeviationByUnit acct_sect unit qty
279 = DeviationByUnit (BalByUnit acct_sect unit qty)
280 deriving (Data, Eq, NFData, Show, Typeable)
282 -- | Return the 'balByUnit' of the given 'Balance' with:
284 -- * @unit@s whose 'sumByUnit_quantity' verifying 'quantity_null' are removed,
286 -- * and remaining @unit@s have their 'sumByUnit_accounts'
287 -- complemented with the 'balByAccount' of the given 'Balance'
288 -- (i.e. now mapping to the 'Balance_Account's __not__ involved to build the 'SumByUnit').
290 :: (Zero qty, Addable qty, Ord acct_sect, Ord unit)
291 => Balance acct_sect unit qty
292 -> DeviationByUnit acct_sect unit qty
293 deviationByUnit Balance
295 , balByUnit=BalByUnit balByUnit
297 let all_accounts = TreeMap.flatten (const ()) balByAccount in
298 let max_accounts = Map.size all_accounts in
301 (\(BalByUnit m) unit SumByUnit{..} ->
303 if quantity_null sumByUnit_quantity
306 case Map.size sumByUnit_accounts of
307 n | n == max_accounts ->
308 Map.insert unit SumByUnit
310 , sumByUnit_accounts = Map.empty
313 let diff = Map.difference all_accounts sumByUnit_accounts in
314 Map.insert unit SumByUnit
316 , sumByUnit_accounts = diff
322 -- ** Balance equilibrium
323 -- | Return the 'Balance' (adjusted by inferred @qty@s)
324 -- of the given @post@s and either:
326 -- * 'Left': the @unit@s which have a non null 'SumByUnit'
327 -- and for which no equibrating 'Balance_Account' can be inferred.
328 -- * 'Right': the given @post@s with inferred @qty@s inserted.
330 ( post ~ MT.Element posts
331 , Seqs.IsSequence posts
332 , Get (Balance_Account acct_sect) post
333 , Has (Balance_Amounts unit qty) post
334 , Zero qty, Addable qty, Negable qty
335 , Ord acct, Ord acct_sect, Ord unit
336 , Get acct (Balance_Account acct_sect)
338 -> ( Balance acct_sect unit qty
339 , Either [(unit, SumByUnit (Balance_Account acct_sect) qty)]
342 let bal_initial = MT.ofoldr balance_postings balance_empty posts in
343 let DeviationByUnit (BalByUnit dev) = deviationByUnit bal_initial in
344 let (bal_adjusted, eithers) =
346 (\unit unit_sum@SumByUnit{..} (bal, lr) ->
347 case Map.size sumByUnit_accounts of
349 let acct = fst $ Map.elemAt 0 sumByUnit_accounts in
350 let qty = quantity_neg sumByUnit_quantity in
351 let amts = Map.singleton unit qty in
352 ( balance_cons (acct, SumByAccount amts) bal
353 , Right (acct, unit, qty) : lr
355 _ -> (bal, Left [(unit, unit_sum)] : lr))
358 let (l, r) = Foldable.accumLeftsAndFoldrRights
359 (\(acct, unit, qty) ->
361 (\_new_ps -> Seqs.fromList . insert_amount (unit, qty) . MT.otoList)
362 (get acct) (assert False undefined))
363 -- NOTE: acct is within bal_initial,
364 -- hence posts already has a mapping for acct.
367 [] -> (bal_adjusted, Right r)
368 _ -> (bal_adjusted, Left l)
371 :: forall post unit qty.
373 , Has (Balance_Amounts unit qty) post
374 ) => (unit, qty) -> [post] -> [post]
375 insert_amount amt@(unit, qty) l =
377 [] -> assert False []
378 -- NOTE: the acct being in bal_initial,
379 -- hence there was at least one post for this acct.
381 let amts :: Balance_Amounts unit qty = get p in
382 if Map.notMember unit amts
383 then set (Map.insert unit qty amts) p:ps
384 else p:insert_amount amt ps
386 -- | Return 'True' if and only if the given 'DeviationByUnit' maps no @unit@.
387 is_equilibrium :: DeviationByUnit acct_sect unit qty -> Bool
388 is_equilibrium (DeviationByUnit (BalByUnit dev)) = Map.null dev
390 -- | Return 'True' if and only if the given 'DeviationByUnit'
391 -- maps only to 'SumByUnit's whose 'sumByUnit_accounts'
392 -- maps exactly one 'Balance_Account'.
393 is_equilibrium_inferrable :: DeviationByUnit acct_sect unit qty -> Bool
394 is_equilibrium_inferrable (DeviationByUnit (BalByUnit dev)) =
395 Foldable.all ((== 1) . Map.size . sumByUnit_accounts) dev
397 -- * Type 'ClusiveBalByAccount'
399 -- | {Ex,In}clusive 'BalByAccount':
400 -- descending propagation of @qty@s accross 'Account's.
401 type ClusiveBalByAccount acct_sect unit qty
402 = TreeMap acct_sect (ClusiveSumByAccount unit qty)
404 -- ** Type 'ClusiveSumByAccount'
406 -- * 'Strict.exclusive': contains the original 'SumByAccount'.
407 -- * 'Strict.inclusive': contains 'quantity_add' folded
408 -- over 'Strict.exclusive' and 'Strict.inclusive'
409 -- of 'TreeMap.node_descendants'
410 type ClusiveSumByAccount unit qty
411 = Strict.Clusive (SumByAccount unit qty)
413 -- | Return the given 'BalByAccount' with:
415 -- * all missing 'Account.parent' 'Account's inserted;
416 -- * and every mapped @qty@ added with any @qty@
417 -- of the 'Account's for which it is 'Account.parent'.
419 :: (Addable qty, Ord acct_sect, Ord unit)
420 => BalByAccount acct_sect unit qty
421 -> ClusiveBalByAccount acct_sect unit qty
422 clusiveBalByAccount =
423 TreeMap.map_by_depth_first
424 (\descendants value ->
425 let exclusive = Strict.fromMaybe mempty value in
430 ( flip $ (<>) . Strict.inclusive
431 . Strict.fromMaybe (assert False undefined)
432 . TreeMap.node_value )
434 TreeMap.nodes descendants
437 -- | Return a 'BalByUnit'
438 -- derived from the given 'ClusiveBalByAccount' balance.
440 -- NOTE: also correct if the 'ClusiveBalByAccount' has been filtered.
441 balByUnit_of_ClusiveBalByAccount
442 :: (Addable qty, Ord acct_sect, Ord unit)
443 => ClusiveBalByAccount acct_sect unit qty
444 -> BalByUnit acct_sect unit qty
445 -> BalByUnit acct_sect unit qty
446 balByUnit_of_ClusiveBalByAccount =
449 go p (TreeMap nodes) bal =
451 (\k TreeMap.Node{TreeMap.node_value, TreeMap.node_descendants} acc ->
453 Strict.Nothing -> go (k:p) node_descendants acc
455 let acct = Seqs.reverse $ TreeMap.path k p in
456 balByUnit_cons (acct, Strict.inclusive a) acc)