1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE FlexibleContexts #-}
3 {-# LANGUAGE NamedFieldPuns #-}
4 {-# LANGUAGE TypeFamilies #-}
5 module Hcompta.Calc.Balance where
7 import Control.Exception (assert)
9 import qualified Data.Foldable
10 import Data.Foldable (Foldable(..))
11 import qualified Data.List
12 import qualified Data.Map.Strict as Data.Map
13 import Data.Map.Strict (Map)
14 import Data.Maybe (fromMaybe)
15 import Data.Typeable ()
17 import qualified Hcompta.Lib.Foldable as Lib.Foldable
18 import qualified Hcompta.Lib.TreeMap as Lib.TreeMap
19 import Hcompta.Lib.TreeMap (TreeMap)
20 import qualified Hcompta.Model.Account as Account
21 import Hcompta.Model.Account (Account)
23 -- * The 'Amount_Sum' type
25 class Num a => Amount a where
26 amount_sign :: a -> Ordering
28 -- | Sum keeping track of negative and positive 'amount's.
29 data Amount_Sum amount unit
31 { amount_sum_negative :: Map unit amount
32 , amount_sum_positive :: Map unit amount
33 , amount_sum_balance :: Map unit amount
34 } deriving (Data, Eq, Show, Typeable)
39 -> Amount_Sum amount unit
42 { amount_sum_negative = Data.Map.filter ((==) LT . amount_sign) a
43 , amount_sum_positive = Data.Map.filter ((==) GT . amount_sign) a
44 , amount_sum_balance = a
48 :: (Amount amount, Ord unit)
49 => Amount_Sum amount unit
50 -> Amount_Sum amount unit
51 -> Amount_Sum amount unit
53 let add = Data.Map.unionWith (flip (+)) in
55 { amount_sum_negative = add (amount_sum_negative a) (amount_sum_negative b)
56 , amount_sum_positive = add (amount_sum_positive a) (amount_sum_positive b)
57 , amount_sum_balance = add (amount_sum_balance a) (amount_sum_balance b)
62 => Amount_Sum amount unit
63 -> Amount_Sum amount unit
66 { amount_sum_negative = amount_sum_positive a
67 , amount_sum_positive = amount_sum_negative a
68 , amount_sum_balance = Data.Map.map negate $ amount_sum_balance a
71 -- * The 'Posting' class
73 -- | A 'posting' used to produce a 'Balance'
74 -- must be an instance of this class.
76 ( Ord (Posting_Unit p)
77 , Amount (Posting_Amount p)
82 posting_account :: p -> Account
83 posting_amounts :: p -> Map (Posting_Unit p) (Posting_Amount p)
84 posting_make :: Account -> Map (Posting_Unit p) (Posting_Amount p) -> p
86 -- * The 'Balance' type
88 -- | Sum by 'Account' and sum by 'unit' of some 'Posting's.
89 data Balance amount unit
91 { balance_by_account :: Balance_by_Account amount unit
92 , balance_by_unit :: Balance_by_Unit amount unit
93 } deriving (Data, Eq, Show, Typeable)
95 type Balance_by_Account amount unit
96 = TreeMap Account.Name
97 (Account_Sum amount unit)
99 -- | A sum of 'amount's,
100 -- concerning a single 'Account'.
101 type Account_Sum amount unit
102 = Data.Map.Map unit amount
104 type Balance_by_Unit amount unit
105 = Map unit (Unit_Sum amount)
107 -- | A sum of 'amount's with their 'Account's involved,
108 -- concerning a single 'unit'.
111 { unit_sum_amount :: Amount_Sum amount () -- ^ The sum of 'amount's for a single 'unit'.
112 , unit_sum_accounts :: Map Account () -- ^ The 'Account's involved to build 'unit_sum_amount'.
113 } deriving (Data, Eq, Show, Typeable)
117 balance :: (Amount amount, Ord unit) => Balance amount unit
120 { balance_by_account = Lib.TreeMap.empty
121 , balance_by_unit = Data.Map.empty
124 -- | Return the given 'Balance'
125 -- updated by the given 'Posting'.
129 , unit ~ Posting_Unit posting
130 , amount ~ Posting_Amount posting
131 ) => posting -> Balance amount unit -> Balance amount unit
134 { balance_by_account =
136 (Data.Map.unionWith (flip (+)))
137 (posting_account post)
138 (posting_amounts post)
139 (balance_by_account bal)
142 (\new old -> Unit_Sum
143 { unit_sum_amount = amount_sum_add
144 (unit_sum_amount old)
145 (unit_sum_amount new)
146 , unit_sum_accounts = Data.Map.unionWith
148 (unit_sum_accounts old)
149 (unit_sum_accounts new)
151 (balance_by_unit bal) $
154 { unit_sum_amount = amount_sum $ Data.Map.singleton () amount
155 , unit_sum_accounts = Data.Map.singleton (posting_account post) ()
157 (posting_amounts post)
160 -- | Return the given 'Balance'
161 -- updated by the given 'Posting's.
165 , unit ~ Posting_Unit posting
166 , amount ~ Posting_Amount posting
167 , Foldable foldable )
169 -> Balance amount unit
170 -> Balance amount unit
171 postings = flip (Data.Foldable.foldr posting)
173 -- | Return the first given 'Balance'
174 -- updated by the second given 'Balance'.
176 :: (Amount amount, Ord unit)
177 => Balance amount unit
178 -> Balance amount unit
179 -> Balance amount unit
182 { balance_by_account =
184 (Data.Map.unionWith (flip (+)))
185 (balance_by_account b0)
186 (balance_by_account b1)
189 (\new old -> Unit_Sum
190 { unit_sum_amount = amount_sum_add
191 (unit_sum_amount old)
192 (unit_sum_amount new)
193 , unit_sum_accounts = Data.Map.unionWith
195 (unit_sum_accounts old)
196 (unit_sum_accounts new)
202 -- * The 'Deviation' type
204 -- | The 'Balance_by_Unit' whose 'unit_sum_amount'
205 -- is not zero and possible 'Account' to 'infer_equilibrium'.
206 newtype Deviation amount unit
207 = Deviation (Balance_by_Unit amount unit)
208 deriving (Data, Eq, Show, Typeable)
210 -- | Return the 'balance_by_unit' of the given 'Balance' with:
212 -- * 'unit's whose 'unit_sum_amount' verifying 'amount_is_zero' removed,
214 -- * and remaining 'unit's having their 'unit_sum_accounts'
215 -- complemented with the 'balance_by_account' of the given 'Balance'
216 -- (i.e. now mapping to 'Account's __not__ involved to build the 'Unit_Sum').
218 :: (Amount amount, Ord unit)
219 => Balance amount unit
220 -> Deviation amount unit
222 let all_accounts = Lib.TreeMap.flatten (const ()) (balance_by_account bal)
223 let max_accounts = Data.Map.size all_accounts
225 Data.Map.foldlWithKey
226 (\m unit Unit_Sum{unit_sum_amount, unit_sum_accounts} ->
227 if EQ == amount_sign (amount_sum_balance unit_sum_amount Data.Map.! ())
230 case Data.Map.size unit_sum_accounts of
231 n | n == max_accounts ->
232 Data.Map.insert unit Unit_Sum
234 , unit_sum_accounts = Data.Map.empty
237 let diff = Data.Map.difference all_accounts unit_sum_accounts
238 Data.Map.insert unit Unit_Sum
240 , unit_sum_accounts = diff
244 (balance_by_unit bal)
246 -- ** The equilibrium
248 -- | Return the 'Balance' (adjusted by inferred 'amount's)
249 -- of the given 'Posting's and either:
251 -- * 'Left': the 'Posting's that cannot be inferred.
252 -- * 'Right': the given 'Posting's with inferred 'amount's inserted.
257 , amount ~ Posting_Amount posting
258 , unit ~ Posting_Unit posting )
259 => Map Account [posting]
260 -> ( Balance amount unit
261 , Either [Unit_Sum amount] (Map Account [posting])
263 infer_equilibrium ps = do
264 let bal_initial = Data.Foldable.foldr postings balance ps
265 let Deviation dev = deviation bal_initial
266 let (bal_adjusted, eithers) =
267 Data.Map.foldrWithKey
268 (\unit unit_sum@(Unit_Sum{unit_sum_amount, unit_sum_accounts})
270 case Data.Map.size unit_sum_accounts of
274 (fst $ Data.Map.elemAt 0 unit_sum_accounts)
275 (Data.Map.mapKeys (\() -> unit) $
277 amount_sum_negate unit_sum_amount) in
281 _ -> (bal, Left [unit_sum] : lr))
285 Lib.Foldable.accumLeftsAndFoldrRights
286 (\p -> Data.Map.insertWith
287 (\pn -> (++) pn . Data.List.filter (not . Data.Map.null . posting_amounts))
288 (posting_account p) [p])
292 [] -> (bal_adjusted, Right r)
293 _ -> (bal_adjusted, Left l)
295 -- | Return 'True' if and only if the given 'Deviation' maps no 'unit'.
296 is_at_equilibrium :: Deviation amount unit -> Bool
297 is_at_equilibrium (Deviation dev) = Data.Map.null dev
299 -- | Return 'True' if and only if the given 'Deviation'
300 -- maps only to 'Unit_Sum's whose 'unit_sum_accounts'
301 -- maps exactly one 'Account'.
302 is_equilibrium_inferrable :: Deviation amount unit -> Bool
303 is_equilibrium_inferrable (Deviation dev) =
305 (\Unit_Sum{unit_sum_accounts} -> Data.Map.size unit_sum_accounts == 1)
308 -- | Return 'True' if and only if the given 'Deviation'
309 -- maps to at least one 'Unit_Sum' whose 'unit_sum_accounts'
310 -- maps more than one 'Account'.
311 is_equilibrium_non_inferrable :: Deviation amount unit -> Bool
312 is_equilibrium_non_inferrable (Deviation dev) =
314 (\Unit_Sum{unit_sum_accounts} -> Data.Map.size unit_sum_accounts > 1)
317 -- * The 'Expanded' type
319 -- | Descending propagation of 'amount's accross 'Account's.
320 type Expanded amount unit
321 = TreeMap Account.Name
322 (Account_Sum_Expanded amount unit)
323 data Account_Sum_Expanded amount unit
324 = Account_Sum_Expanded
325 { exclusive :: Map unit amount
326 , inclusive :: Amount_Sum amount unit -- ^ 'amount_sum_add' folded over 'exclusive' and 'inclusive' of 'Lib.TreeMap.node_descendants'
328 deriving (Data, Eq, Show, Typeable)
330 -- | Return the given 'Balance_by_Account' with:
332 -- * all missing 'Account.ascending' 'Account's inserted,
334 -- * and every mapped 'amount'
335 -- added with any 'amount'
336 -- of the 'Account'sā for which it is 'Account.ascending'.
338 ( Amount amount, Ord unit )
339 => Balance_by_Account amount unit
340 -> Expanded amount unit
342 let from_value = fromMaybe (assert False undefined) . Lib.TreeMap.node_value in
343 Lib.TreeMap.map_by_depth_first
344 (\descendants value ->
345 let nodes = Lib.TreeMap.nodes descendants in
346 let exclusive = fromMaybe Data.Map.empty value in
351 (amount_sum_add . inclusive . from_value)
352 (amount_sum exclusive) nodes