1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE MultiParamTypeClasses #-}
4 {-# LANGUAGE NamedFieldPuns #-}
5 {-# LANGUAGE OverloadedStrings #-}
6 {-# LANGUAGE Rank2Types #-}
7 {-# LANGUAGE ScopedTypeVariables #-}
8 {-# LANGUAGE TupleSections #-}
9 {-# LANGUAGE TypeFamilies #-}
10 {-# OPTIONS_GHC -fno-warn-orphans #-}
12 module Balance.Test where
14 import Control.Arrow ((***))
16 import Data.Either (Either(..), either, isRight)
17 import Data.Eq (Eq(..))
18 import Data.Function (($), (.), id, const)
19 import Data.Functor ((<$>))
20 import Data.Map.Strict (Map)
21 import Data.Maybe (Maybe(..))
22 import Data.Ord (Ord(..))
23 import Data.String (String)
24 import Data.Text (Text)
25 import Data.Tuple (snd)
26 import Prelude (Integer, error)
28 import Test.Tasty.HUnit
29 import Text.Show (Show(..))
30 import qualified Data.List as List
31 import qualified Data.Map.Strict as Map
32 import qualified Data.NonNull as NonNull
33 import qualified Data.Text as Text
35 import qualified Data.TreeMap.Strict as TreeMap
36 import qualified Hcompta as H
37 import qualified Hcompta.Lib.Strict as Strict
39 elide :: String -> String
40 elide s | List.length s > 42 = List.take 42 s List.++ ['…']
44 type Quantity = Integer
45 amounts :: (H.Addable q, Ord u) => [(u, q)] -> Map u q
46 amounts = Map.fromListWith (H.+)
47 pol :: H.Polarizable t => (forall q. q -> (Unit, q)) -> (t -> (Unit, H.Polarized t))
48 pol u = u . H.polarize
57 account :: Text -> TreeMap.Path Text
58 account txt = case go txt of
60 a:as -> NonNull.ncons a as
70 let (chunk, rest) = Text.break (== sep) t in
73 type Posting = (Account, [(Unit, Quantity)])
74 instance H.To (TreeMap.Path a) (TreeMap.Path a) where to = id
77 tests = testGroup "Balance"
78 [ testGroup "balanceOf" $
79 let (==>) (input::[Posting])
80 ( byAccount::[(Account, [(Unit, H.Polarized Quantity)])]
81 , byUnit::[(Unit, (H.Polarized Quantity, [Account]))] ) =
82 testCase (elide $ show input) $
83 List.foldl (H.+=) H.zero
84 ((account *** Map.map H.polarize . amounts) <$> input) @?=
86 { H.balByAccount = TreeMap.from_List const $
87 (account *** amounts) <$> byAccount
88 , H.balByUnit = Map.fromList $
89 (id *** (\(q, as) -> H.SumByUnit q $ Map.fromList $ ((,()) . account <$> as))) <$> byUnit
92 [ [ ("/A", [usd 1]) ] ==> (,)
93 [ ("/A", [pol usd 1]) ]
94 [ usd (H.polarize 1, ["/A"]) ]
96 , ("/A", [usd (-1)]) ] ==> (,)
97 [ ("/A", [usd $ H.PolBoth (-1) 1]) ]
98 [ usd (H.PolBoth (-1) 1, ["/A"]) ]
100 , ("/A", [eur (-1)]) ] ==> (,)
103 [ usd (H.polarize 1, ["/A"])
104 , eur (H.polarize (-1), ["/A"]) ]
106 , ("/B", [usd (-1)]) ] ==> (,)
107 [ ("/A", [pol usd 1])
108 , ("/B", [pol usd (-1)]) ]
109 [ usd (H.PolBoth (-1) 1, ["/A", "/B"]) ]
111 , ("/B", [eur (-1)]) ] ==> (,)
112 [ ("/A", [pol usd 1])
113 , ("/B", [pol eur (-1)]) ]
114 [ usd (H.polarize 1, ["/A"])
115 , eur (H.polarize (-1), ["/B"]) ]
117 , ("/B", [usd 1]) ] ==> (,)
118 [ ("/A", [pol usd 1])
119 , ("/B", [pol usd 1]) ]
120 [ usd (H.polarize 2, ["/A", "/B"]) ]
121 , [ ("/A", [usd 1, eur 2])
122 , ("/A", [usd (-1), eur (-2)]) ] ==> (,)
123 [ ("/A", [ usd $ H.PolBoth (-1) 1
124 , eur $ H.PolBoth (-2) 2 ]) ]
125 [ usd (H.PolBoth (-1) 1, ["/A"])
126 , eur (H.PolBoth (-2) 2, ["/A"]) ]
127 , [ ("/A", [usd 1, eur 2, gbp 3])
128 , ("/B", [usd (-1), eur (-2), gbp (-3)]) ] ==> (,)
132 , ("/B", [ pol usd (-1)
135 [ usd (H.PolBoth (-1) 1, ["/A", "/B"])
136 , eur (H.PolBoth (-2) 2, ["/A", "/B"])
137 , gbp (H.PolBoth (-3) 3, ["/A", "/B"]) ]
139 , testGroup "unionBal" $
142 [( [(Account, [(Unit, H.Polarized Quantity)])]
143 , [(Unit, (H.Polarized Quantity, [Account]))] )])
145 ( [(Account, [(Unit, H.Polarized Quantity)])]
146 , [(Unit, (H.Polarized Quantity, [Account]))] )) =
147 testCase (elide $ show input) $
148 List.foldl (H.+) H.zero (bal <$> input) @?= bal expected
150 bal (byAccount, byUnit) =
152 { H.balByAccount = TreeMap.from_List const $
153 (account *** amounts) <$> byAccount
154 , H.balByUnit = Map.fromList $
155 (id *** (\(q, as) -> H.SumByUnit q $ Map.fromList $ ((,()) . account <$> as))) <$> byUnit
159 [ ("/A", [pol usd 1]) ]
160 [ usd (H.polarize 1, ["/A"]) ]
162 [ ("/A", [pol usd 1]) ]
163 [ usd (H.polarize 1, ["/A"]) ]
165 [ ("/A", [pol usd 2]) ]
166 [ usd (H.polarize 2, ["/A"]) ]
168 [ ("/A", [pol usd 1]) ]
169 [ usd (H.polarize 1, ["/A"]) ]
171 [ ("/B", [pol usd 1]) ]
172 [ usd (H.polarize 1, ["/B"]) ]
174 [ ("/A", [pol usd 1])
175 , ("/B", [pol usd 1]) ]
176 [ usd (H.polarize 2, ["/A", "/B"]) ]
178 [ ("/A", [pol usd 1]) ]
179 [ usd (H.polarize 1, ["/A"]) ]
181 [ ("/B", [pol eur 1]) ]
182 [ eur (H.polarize 1, ["/B"]) ]
184 [ ("/A", [pol usd 1])
185 , ("/B", [pol eur 1]) ]
186 [ usd (H.polarize 1, ["/A"])
187 , eur (H.polarize 1, ["/B"]) ]
189 [ ("/A", [pol usd 1, pol eur 2]) ]
190 [ usd (H.polarize 1, ["/A"])
191 , eur (H.polarize 2, ["/A"]) ]
193 [ ("/B", [pol usd (-1), pol eur (-2)]) ]
194 [ usd (H.polarize (-1), ["/B"])
195 , eur (H.polarize (-2), ["/B"]) ]
197 [ ("/A", [pol usd 1 , pol eur 2])
198 , ("/B", [pol usd (-1), pol eur (-2)]) ]
199 [ usd (H.PolBoth (-1) 1, ["/A", "/B"])
200 , eur (H.PolBoth (-2) 2, ["/A", "/B"]) ]
202 [ ("/A", [pol usd 1, pol eur 2]) ]
203 [ usd (H.polarize 1, ["/A"])
204 , eur (H.polarize 2, ["/A"]) ]
206 [ ("/B", [pol usd (-1), pol eur (-2)]) ]
207 [ usd (H.polarize (-1), ["/B"])
208 , eur (H.polarize (-2), ["/B"]) ]
210 [ ("/C", [pol gbp 3]) ]
211 [ gbp (H.polarize 3, ["/C"]) ]
213 [ ("/A", [pol usd 1 , pol eur 2])
214 , ("/B", [pol usd (-1), pol eur (-2)])
215 , ("/C", [pol gbp 3]) ]
216 [ usd (H.PolBoth (-1) 1, ["/A", "/B"])
217 , eur (H.PolBoth (-2) 2, ["/A", "/B"])
218 , gbp (H.polarize 3, ["/C"]) ]
220 , testGroup "clusiveBalByAccount" $
222 (input::[(Account, [(Unit, H.Polarized Quantity)])])
223 (expected::[(Account, ( [(Unit, H.Polarized Quantity)]
224 , [(Unit, H.Polarized Quantity)] ))]) =
225 testCase (elide $ show input) $
226 H.clusiveBalByAccount (bal input) @?= exbal expected
229 TreeMap.from_List const $
230 (account *** amounts) <$> byAccount
232 TreeMap.from_List const $
233 (account *** (\(e, i) -> Strict.Clusive (amounts e) (amounts i) )) <$> byAccount
236 , [ ("/A", [pol usd 1]) ] ==>
237 [ ("/A", (,) [pol usd 1] [pol usd 1]) ]
238 , [ ("/A/A", [pol usd 1]) ] ==>
239 [ ("/A", (,) [] [pol usd 1])
240 , ("/A/A", (,) [pol usd 1] [pol usd 1]) ]
241 , [ ("/A/B", [pol usd 1]) ] ==>
242 [ ("/A", (,) [] [pol usd 1])
243 , ("/A/B", (,) [pol usd 1] [pol usd 1]) ]
244 , [ ("/A/B/C", [pol usd 1]) ] ==>
245 [ ("/A", (,) [] [pol usd 1])
246 , ("/A/B", (,) [] [pol usd 1])
247 , ("/A/B/C", (,) [pol usd 1] [pol usd 1]) ]
248 , [ ("/A/B", [pol usd (-1)])
249 , ("/A/B/C", [pol usd 1])
251 [ ("/A", (,) [] [usd $ H.PolBoth (-1) 1])
252 , ("/A/B", (,) [pol usd (-1)] [usd $ H.PolBoth (-1) 1])
253 , ("/A/B/C", (,) [pol usd 1] [pol usd 1]) ]
254 , [ ("/A/B", [pol usd 1])
255 , ("/A/B/C", [pol usd 1])
256 , ("/A/B/D", [pol usd 1])
258 [ ("/A", (,) [] [pol usd 3])
259 , ("/A/B", (,) [pol usd 1] [pol usd 3])
260 , ("/A/B/C", (,) [pol usd 1] [pol usd 1])
261 , ("/A/B/D", (,) [pol usd 1] [pol usd 1]) ]
262 , [ ("/A/B", [pol usd 1])
263 , ("/A/C", [pol usd 1])
264 , ("/D/B", [pol usd 1])
266 [ ("/A", (,) [] [pol usd 2])
267 , ("/A/B", (,) [pol usd 1] [pol usd 1])
268 , ("/A/C", (,) [pol usd 1] [pol usd 1])
269 , ("/D", (,) [] [pol usd 1])
270 , ("/D/B", (,) [pol usd 1] [pol usd 1]) ]
272 , testGroup "deviationByUnit" $
273 let (==>) (input::[Posting])
274 (expected::[(Unit, (H.Polarized Quantity, [Account]))]) =
275 testCase (elide $ show input) $
281 H.DeviationByUnit balDev
284 TreeMap.from_List const $
285 (account *** (H.polarize <$>) . amounts) <$> input
286 balByUnit = H.sum balByAccount
290 H.SumByUnit q $ Map.fromList $
291 ((,()) . account <$> as))
295 , [ ("/A", [usd 1]) ] ==>
296 [usd (H.polarize 1, [])]
299 [usd (H.polarize 1, ["/B"])]
302 [usd (H.polarize 1, ["/A/B"])]
304 , ("/A/B", [usd (-1)]) ] ==>
307 , ("/A/B", [eur (-1)]) ] ==>
308 [ usd (H.polarize 1, ["/A/B"])
309 , eur (H.polarize (-1), ["/A"])
312 , ("/B", [usd 1]) ] ==>
313 [usd (H.polarize 2, [])]
315 , ("/B", [eur 2]) ] ==>
316 [ usd (H.polarize 1, ["/B"])
317 , eur (H.polarize 2, ["/A"])
319 , [ ("/A", [usd 1, eur 2])
320 , ("/B", [usd (-1), eur 2]) ] ==>
321 [ eur (H.polarize 4, []) ]
322 , [ ("/A", [usd 1, eur 2])
323 , ("/B", [usd (-1), eur 2])
324 , ("/C", [gbp 3]) ] ==>
325 [ eur (H.polarize 4, ["/C"])
326 , gbp (H.polarize 3, ["/A", "/B"]) ]
328 , testGroup "equilibrium" $
329 let (==>) (input::[Posting])
330 (expected::Either [(Unit, (H.Polarized Quantity, [Account]))] [Posting]) =
331 testCase (elide $ show input) $
332 let o = snd (H.equilibrium $ postings input) in
333 let e = either (Left . units) (Right . postings) expected in
334 let is = H.isEquilibriumInferrable $
340 (isRight o, o) @?= (is, e)
344 -> Map (TreeMap.Path Text)
346 , Map Unit (H.Polarized Quantity) )]
350 let a = account acct in (a,)
351 [(a, H.polarize <$> amounts amts)] ) <$>)
353 :: [(Unit, (H.Polarized Quantity, [Account]))]
354 -> [(Unit, H.SumByUnit (TreeMap.Path Text) (H.Polarized Quantity))]
356 ((id *** (\(q, as) ->
357 H.SumByUnit q $ Map.fromList $
358 ((,()) . account <$> as))) <$>)
360 TreeMap.from_List const $
361 (account *** (H.polarize <$>) . amounts) <$> input
362 balByUnit = H.sum balByAccount
365 , [ ("/A", [usd 1]) ] ==> Left
366 [ usd (H.polarize 1, []) ]
368 , ("/B", []) ] ==> Right
370 , ("/B", [usd (-1)]) ]
372 , ("/A/B", []) ] ==> Right
374 , ("/A/B", [usd (-1)]) ]
376 , ("/A/B", [usd (-1)]) ] ==> Right
378 , ("/A/B", [usd (-1)]) ]
380 , ("/A/B", [eur (-1)]) ] ==> Right
381 [ ("/A", [usd 1, eur 1])
382 , ("/A/B", [usd (-1), eur (-1)]) ]
384 , ("/B", [usd 1]) ] ==> Left
385 [ usd (H.polarize 2, []) ]
387 , ("/B", [eur 2]) ] ==> Right
388 [ ("/A", [usd 1, eur (-2)])
389 , ("/B", [usd (-1), eur 2]) ]
390 , [ ("/A", [usd 1, eur 2])
391 , ("/B", [usd (-1), eur 2]) ] ==> Left
392 [ eur (H.polarize 4, []) ]
393 , [ ("/A", [usd 1, eur 2])
394 , ("/B", [usd (-1), eur 2])
395 , ("/C", [gbp 3]) ] ==> Left
396 [ gbp (H.polarize 3, ["/A", "/B"]) ]