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)
11 import Data.Typeable ()
12 import Data.Maybe (fromMaybe)
13 import qualified GHC.Num
15 import qualified Hcompta.Model as Model ()
16 import qualified Hcompta.Model.Account as Account
17 import qualified Hcompta.Lib.TreeMap as Lib.TreeMap
18 import Hcompta.Model.Account (Account)
19 import qualified Hcompta.Model.Amount as Amount
20 import Hcompta.Model.Amount (Amount, Unit)
21 import qualified Hcompta.Model.Transaction as Transaction
22 import Hcompta.Model.Transaction (Transaction, Posting)
23 import qualified Hcompta.Model.Transaction.Posting as Posting
24 import qualified Hcompta.Model.Journal as Journal
25 import Hcompta.Model.Journal (Journal)
27 -- * The 'Balance' type
29 -- | Sum by 'Account' and sum by 'Unit' of some 'Posting's.
32 { by_account :: By_Account
34 } deriving (Data, Eq, Read, Show, Typeable)
37 = Lib.TreeMap.TreeMap Account.Name Account_Sum
38 -- | A sum of 'Amount's,
39 -- concerning a single 'Account'.
44 = Map Amount.Unit Unit_Sum
45 -- | A sum of 'Amount's with their 'Account's involved,
46 -- concerning a single 'Unit'.
49 { amount :: Amount -- ^ The sum of 'Amount's for a single 'Unit'.
50 , accounts :: Map Account () -- ^ The 'Account's involved to build 'amount'.
51 } deriving (Data, Eq, Read, Show, Typeable)
58 { by_account = Lib.TreeMap.empty
59 , by_unit = Data.Map.empty
62 nil_By_Account :: By_Account
66 nil_By_Unit :: By_Unit
70 nil_Account_Sum :: Account_Sum
74 nil_Unit_Sum :: Unit_Sum
77 { accounts = Data.Map.empty
81 -- | Return a tuple associating the given 'Unit_Sum' with its 'Unit'.
82 assoc_unit_sum :: Unit_Sum -> (Unit, Unit_Sum)
83 assoc_unit_sum s = (Amount.unit $ amount s, s)
85 -- | Return a 'Map' associating the given 'Unit_Sum' with their respective 'Unit'.
86 by_Unit_from_List :: [Unit_Sum] -> By_Unit
87 by_Unit_from_List balances =
90 { amount=(GHC.Num.+) (amount x) (amount y)
91 , accounts=Data.Map.union (accounts x) (accounts y)
93 Data.List.map assoc_unit_sum balances
95 -- ** Incremental constructors
97 -- | Return the given 'Balance'
98 -- updated by the given 'Posting'.
99 posting :: Posting -> Balance -> Balance
100 posting post balance =
104 (Data.Map.unionWith (GHC.Num.+))
105 (Posting.account post)
106 (Posting.amounts post)
111 { amount = (GHC.Num.+) (amount x) (amount y)
112 , accounts = Data.Map.union (accounts x) (accounts y)
118 , accounts = Data.Map.singleton (Posting.account post) ()
120 (Posting.amounts post)
123 -- | Return the given 'Balance'
124 -- updated by the given 'Posting's.
125 postings :: (Foldable to, Foldable ti) => to (ti Posting) -> Balance -> Balance
126 postings = flip $ Data.Foldable.foldr (flip (Data.Foldable.foldr posting))
128 -- | Return the given 'Balance'
129 -- updated by the 'Transaction.postings'
130 -- of the given 'Transaction'.
131 transaction :: Transaction -> Balance -> Balance
132 transaction = postings . Transaction.postings
134 -- | Return the given 'Balance'
135 -- updated by the 'Transaction.postings'
136 -- and 'Transaction.virtual_postings'
137 -- and 'Transaction.balanced_virtual_postings'
138 -- of the given 'Transaction'.
139 transaction_with_virtual :: Transaction -> Balance -> Balance
140 transaction_with_virtual tr =
141 postings (Transaction.balanced_virtual_postings tr) .
142 postings (Transaction.virtual_postings tr) .
143 postings (Transaction.postings tr)
145 -- | Return the given 'Balance'
146 -- updated by the 'Transaction.balanced_virtual_postings' of the given 'Transaction'.
147 transaction_balanced_virtual :: Transaction -> Balance -> Balance
148 transaction_balanced_virtual =
149 postings . Transaction.balanced_virtual_postings
151 -- | Return the given 'Balance'
152 -- updated by the 'Journal.transactions'
153 -- of the given 'Journal',
154 -- through 'transaction'.
155 journal :: Journal -> Balance -> Balance
156 journal jour balance =
158 (Data.List.foldl (flip transaction))
160 (Journal.transactions jour)
162 -- | Return the given 'Balance'
163 -- updated by the 'Journal.transactions'
164 -- of the given 'Journal',
165 -- through 'transaction'.
166 journal_with_virtual :: Journal -> Balance -> Balance
167 journal_with_virtual jour balance =
169 (Data.List.foldl (flip transaction_with_virtual))
171 (Journal.transactions jour)
173 -- | Return the first given 'Balance'
174 -- updated by the second given 'Balance'.
175 union :: Balance -> Balance -> Balance
180 (Data.Map.unionWith (GHC.Num.+))
186 { amount = (GHC.Num.+) (amount x) (amount y)
187 , accounts = Data.Map.union (accounts x) (accounts y)
193 -- * The 'Equilibre' type
195 -- | See 'equilibre'.
198 deriving (Data, Eq, Read, Show, Typeable)
200 -- | Return the 'by_unit' of the given 'Balance' with:
202 -- * 'Unit's whose 'Unit_Sum' 'amount' verifying 'Amount.is_zero' removed,
204 -- * and remaining 'Unit's having their 'Unit_Sum' 'accounts'
205 -- complemented with the 'by_account' of the given 'Balance'
206 -- (i.e. now mapping to accounts __not__ involved to build the 'Unit_Sum').
207 equilibre :: Balance -> Equilibre
208 equilibre balance = do
209 let all_accounts = Lib.TreeMap.flatten (const ()) (by_account balance)
210 let max_accounts = Data.Map.size all_accounts
212 Data.Map.foldlWithKey
213 (\m unit Unit_Sum{amount, accounts} ->
214 if Amount.is_zero amount
217 case Data.Map.size accounts of
218 n | n == max_accounts ->
219 Data.Map.insert unit Unit_Sum{amount, accounts=Data.Map.empty} m
221 let diff = Data.Map.difference all_accounts accounts
222 Data.Map.insert unit Unit_Sum{amount, accounts=diff} m
229 -- | Return 'True' if and only if the given 'Equilibre' satisfies:
231 -- * 'is_equilibrated',
232 -- * or 'is_inferrable'.
233 is_equilibrable :: Equilibre -> Bool
234 is_equilibrable e@(Equilibre eq) =
235 Data.Map.null eq || is_inferrable e
237 -- | Return 'True' if and only if the given 'Equilibre' maps no 'Unit'.
238 is_equilibrated :: Equilibre -> Bool
239 is_equilibrated (Equilibre eq) = Data.Map.null eq
241 -- | Return 'True' if and only if the given 'Equilibre'
242 -- maps only to 'Unit_Sum's whose 'accounts'
243 -- maps exactly one 'Account'.
244 is_inferrable :: Equilibre -> Bool
245 is_inferrable (Equilibre eq) =
247 (\Unit_Sum{accounts} -> Data.Map.size accounts == 1)
250 -- | Return 'True' if and only if the given 'Equilibre'
251 -- maps to at least one 'Unit_Sum's whose 'accounts'
252 -- maps more than one 'Account'.
253 is_non_inferrable :: Equilibre -> Bool
254 is_non_inferrable (Equilibre eq) =
256 (\Unit_Sum{accounts} -> Data.Map.size accounts > 1)
259 -- * The 'Expanded' type
262 type Expanded = Lib.TreeMap.TreeMap Account.Name Account_Sum_Expanded
263 data Account_Sum_Expanded
264 = Account_Sum_Expanded
265 { inclusive :: Amount.By_Unit
266 , exclusive :: Amount.By_Unit
268 deriving (Data, Eq, Read, Show, Typeable)
270 -- | Return the given 'By_Account' with:
272 -- * all missing 'Account.ascending' 'Account's inserted,
274 -- * and every mapped Amount.'Amount.By_Unit'
275 -- added with any Amount.'Amount.By_Unit'
276 -- of the 'Account'sā for which it is 'Account.ascending'.
277 expand :: By_Account -> Expanded
279 Lib.TreeMap.map_by_depth_first
280 (\descendants value ->
281 let exc = fromMaybe Data.Map.empty value in
286 ( Data.Map.unionWith (GHC.Num.+)
288 . fromMaybe (error "Oops, should not happen in: Hcompta.Calc.Balance.expand")
289 . Lib.TreeMap.node_value) )
290 exc $ Lib.TreeMap.nodes $ descendants