1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE NamedFieldPuns #-}
3 {-# LANGUAGE ScopedTypeVariables #-}
4 module Hcompta.Calc.Balance where
7 import qualified Data.Foldable
8 import qualified Data.List
9 import qualified Data.Map.Strict as Data.Map
10 import Data.Map.Strict (Map)
12 import Data.Typeable ()
13 import Data.Maybe (fromMaybe)
14 import qualified GHC.Num
16 import qualified Hcompta.Model as Model ()
17 import qualified Hcompta.Model.Account as Account
18 import qualified Hcompta.Lib.Foldable as Lib.Foldable
19 import qualified Hcompta.Lib.TreeMap as Lib.TreeMap
20 import Hcompta.Model.Account (Account)
21 import qualified Hcompta.Model.Amount as Amount
22 import Hcompta.Model.Amount (Amount, Unit)
23 import qualified Hcompta.Model.Transaction as Transaction
24 import Hcompta.Model.Transaction (Transaction, Posting)
25 import qualified Hcompta.Model.Transaction.Posting as Posting
26 import qualified Hcompta.Model.Journal as Journal
27 import Hcompta.Model.Journal (Journal)
29 -- * The 'Balance' type
31 -- | Sum by 'Account' and sum by 'Unit' of some 'Posting's.
34 { by_account :: By_Account
36 } deriving (Data, Eq, Read, Show, Typeable)
39 = Lib.TreeMap.TreeMap Account.Name Account_Sum
40 -- | A sum of 'Amount's,
41 -- concerning a single 'Account'.
46 = Map Amount.Unit Unit_Sum
47 -- | A sum of 'Amount's with their 'Account's involved,
48 -- concerning a single 'Unit'.
51 { amount :: Amount -- ^ The sum of 'Amount's for a single 'Unit'.
52 , accounts :: Map Account () -- ^ The 'Account's involved to build 'amount'.
53 } deriving (Data, Eq, Read, Show, Typeable)
60 { by_account = Lib.TreeMap.empty
61 , by_unit = Data.Map.empty
64 nil_By_Account :: By_Account
68 nil_By_Unit :: By_Unit
72 nil_Account_Sum :: Account_Sum
76 nil_Unit_Sum :: Unit_Sum
79 { accounts = Data.Map.empty
83 -- | Return a tuple associating the given 'Unit_Sum' with its 'Unit'.
84 assoc_unit_sum :: Unit_Sum -> (Unit, Unit_Sum)
85 assoc_unit_sum s = (Amount.unit $ amount s, s)
87 -- | Return a 'Map' associating the given 'Unit_Sum' with their respective 'Unit'.
88 by_Unit_from_List :: [Unit_Sum] -> By_Unit
89 by_Unit_from_List balances =
92 { amount=(GHC.Num.+) (amount x) (amount y)
93 , accounts=Data.Map.union (accounts x) (accounts y)
95 Data.List.map assoc_unit_sum balances
97 -- ** Incremental constructors
99 -- | Return the given 'Balance'
100 -- updated by the given 'Posting'.
101 posting :: Posting -> Balance -> Balance
102 posting post balance =
106 (Data.Map.unionWith (GHC.Num.+))
107 (Posting.account post)
108 (Posting.amounts post)
113 { amount = (GHC.Num.+) (amount x) (amount y)
114 , accounts = Data.Map.union (accounts x) (accounts y)
120 , accounts = Data.Map.singleton (Posting.account post) ()
122 (Posting.amounts post)
125 -- | Return the given 'Balance'
126 -- updated by the given 'Posting's.
127 postings :: (Foldable to, Foldable ti) => to (ti Posting) -> Balance -> Balance
128 postings = flip $ Data.Foldable.foldr (flip (Data.Foldable.foldr posting))
130 -- | Return the given 'Balance'
131 -- updated by the 'Transaction.postings'
132 -- of the given 'Transaction'.
133 transaction :: Transaction -> Balance -> Balance
134 transaction = postings . Transaction.postings
136 -- | Return the given 'Balance'
137 -- updated by the 'Transaction.postings'
138 -- and 'Transaction.virtual_postings'
139 -- and 'Transaction.balanced_virtual_postings'
140 -- of the given 'Transaction'.
141 transaction_with_virtual :: Transaction -> Balance -> Balance
142 transaction_with_virtual tr =
143 postings (Transaction.balanced_virtual_postings tr) .
144 postings (Transaction.virtual_postings tr) .
145 postings (Transaction.postings tr)
147 -- | Return the given 'Balance'
148 -- updated by the 'Transaction.balanced_virtual_postings' of the given 'Transaction'.
149 transaction_balanced_virtual :: Transaction -> Balance -> Balance
150 transaction_balanced_virtual =
151 postings . Transaction.balanced_virtual_postings
153 -- | Return the given 'Balance'
154 -- updated by the 'Journal.transactions'
155 -- of the given 'Journal',
156 -- through 'transaction'.
157 journal :: Journal -> Balance -> Balance
158 journal jour balance =
160 (Data.List.foldl (flip transaction))
162 (Journal.transactions jour)
164 -- | Return the given 'Balance'
165 -- updated by the 'Journal.transactions'
166 -- of the given 'Journal',
167 -- through 'transaction'.
168 journal_with_virtual :: Journal -> Balance -> Balance
169 journal_with_virtual jour balance =
171 (Data.List.foldl (flip transaction_with_virtual))
173 (Journal.transactions jour)
175 -- | Return the first given 'Balance'
176 -- updated by the second given 'Balance'.
177 union :: Balance -> Balance -> Balance
182 (Data.Map.unionWith (GHC.Num.+))
188 { amount = (GHC.Num.+) (amount x) (amount y)
189 , accounts = Data.Map.union (accounts x) (accounts y)
195 -- * The 'Equilibre' type
197 -- | See 'equilibre'.
200 deriving (Data, Eq, Read, Show, Typeable)
202 -- | Return the 'by_unit' of the given 'Balance' with:
204 -- * 'Unit's whose 'Unit_Sum' 'amount' verifying 'Amount.is_zero' removed,
206 -- * and remaining 'Unit's having their 'Unit_Sum' 'accounts'
207 -- complemented with the 'by_account' of the given 'Balance'
208 -- (i.e. now mapping to accounts __not__ involved to build the 'Unit_Sum').
209 equilibre :: Balance -> Equilibre
210 equilibre balance = do
211 let all_accounts = Lib.TreeMap.flatten (const ()) (by_account balance)
212 let max_accounts = Data.Map.size all_accounts
214 Data.Map.foldlWithKey
215 (\m unit Unit_Sum{amount, accounts} ->
216 if Amount.is_zero amount
219 case Data.Map.size accounts of
220 n | n == max_accounts ->
221 Data.Map.insert unit Unit_Sum{amount, accounts=Data.Map.empty} m
223 let diff = Data.Map.difference all_accounts accounts
224 Data.Map.insert unit Unit_Sum{amount, accounts=diff} m
232 -- * 'Left': the 'Posting.By_Account's (lazy list) that cannot be inferred
233 -- * 'Right': the given 'Posting.By_Account's with inferred 'Amount's inserted.
235 :: Posting.By_Account
238 infer_equilibre ps = do
239 let bal = postings ps nil
240 let Equilibre equ = equilibre bal
241 (\(l, r) -> case l of
244 Lib.Foldable.accumLeftsAndFoldrRights
245 (\p -> Data.Map.insertWith
246 (\pn -> (++) pn . Data.List.filter (not . Data.Map.null . Posting.amounts))
247 (Posting.account p) [p])
250 (\unit_sum@(Unit_Sum{ amount=amt, accounts }) acc ->
251 case Data.Map.size accounts of
253 1 -> (Right $ (Posting.nil $ fst $ Data.Map.elemAt 0 accounts)
254 { Posting.amounts = Amount.from_List [negate amt] }
256 _ -> Left [unit_sum]:acc
263 -- | Return 'True' if and only if the given 'Equilibre' satisfies:
265 -- * 'is_equilibrated',
266 -- * or 'is_inferrable'.
267 is_equilibrable :: Equilibre -> Bool
268 is_equilibrable e@(Equilibre eq) =
269 Data.Map.null eq || is_inferrable e
271 -- | Return 'True' if and only if the given 'Equilibre' maps no 'Unit'.
272 is_equilibrated :: Equilibre -> Bool
273 is_equilibrated (Equilibre eq) = Data.Map.null eq
275 -- | Return 'True' if and only if the given 'Equilibre'
276 -- maps only to 'Unit_Sum's whose 'accounts'
277 -- maps exactly one 'Account'.
278 is_inferrable :: Equilibre -> Bool
279 is_inferrable (Equilibre eq) =
281 (\Unit_Sum{accounts} -> Data.Map.size accounts == 1)
284 -- | Return 'True' if and only if the given 'Equilibre'
285 -- maps to at least one 'Unit_Sum's whose 'accounts'
286 -- maps more than one 'Account'.
287 is_non_inferrable :: Equilibre -> Bool
288 is_non_inferrable (Equilibre eq) =
290 (\Unit_Sum{accounts} -> Data.Map.size accounts > 1)
293 -- * The 'Expanded' type
296 type Expanded = Lib.TreeMap.TreeMap Account.Name Account_Sum_Expanded
297 data Account_Sum_Expanded
298 = Account_Sum_Expanded
299 { inclusive :: Amount.By_Unit
300 , exclusive :: Amount.By_Unit
302 deriving (Data, Eq, Read, Show, Typeable)
304 -- | Return the given 'By_Account' with:
306 -- * all missing 'Account.ascending' 'Account's inserted,
308 -- * and every mapped Amount.'Amount.By_Unit'
309 -- added with any Amount.'Amount.By_Unit'
310 -- of the 'Account'sā for which it is 'Account.ascending'.
311 expand :: By_Account -> Expanded
313 Lib.TreeMap.map_by_depth_first
314 (\descendants value ->
315 let exc = fromMaybe Data.Map.empty value in
320 ( Data.Map.unionWith (GHC.Num.+)
322 . fromMaybe (error "Oops, should not happen in: Hcompta.Calc.Balance.expand")
323 . Lib.TreeMap.node_value) )
324 exc $ Lib.TreeMap.nodes $ descendants