1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE FlexibleContexts #-}
3 {-# LANGUAGE FlexibleInstances #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE TypeFamilies #-}
6 {-# OPTIONS_GHC -fno-warn-orphans #-}
7 module Hcompta.Amount where
11 import qualified Data.List
12 import qualified Data.Map.Strict as Data.Map
13 import Data.Map.Strict (Map)
14 import qualified Data.Foldable
15 import Data.Typeable ()
17 import qualified Hcompta.Balance as Balance
18 import qualified Hcompta.GL as GL
19 import qualified Hcompta.Amount.Quantity as Quantity
20 import qualified Hcompta.Amount.Style as Style
21 import qualified Hcompta.Amount.Unit as Unit
23 -- * Type synonyms to submodules
25 type Quantity = Quantity.Quantity
26 type Style = Style.Style
29 -- * The 'Amount' type
33 { quantity :: !Quantity
36 } deriving (Data, Show, Typeable)
37 instance NFData (Amount) where
38 rnf (Amount q s u) = rnf q `seq` rnf s `seq` rnf u
40 instance Eq Amount where
42 Amount{quantity=q0, unit=u0}
43 Amount{quantity=q1, unit=u1} =
47 EQ -> (==) q0 q1 -- NOTE: MUST already be rounded to Style.precision
49 instance Ord Amount where
51 Amount{quantity=q0, unit=u0}
52 Amount{quantity=q1, unit=u1} =
56 EQ -> compare q0 q1 -- NOTE: MUST already be rounded to Style.precision
58 instance Balance.Amount Amount where
59 type Amount_Unit Amount = Unit
60 amount_null = (==) Quantity.zero . quantity
62 amount_negate = negate
64 instance Balance.Amount (Map Unit Amount) where
65 type Amount_Unit (Map Unit Amount) = Unit
66 amount_null = Data.Foldable.all ((==) Quantity.zero . quantity)
67 amount_add = Data.Map.unionWith (+)
68 amount_negate = Data.Map.map negate
70 instance GL.Amount Amount where
71 type Amount_Unit Amount = Unit
74 instance GL.Amount (Map Unit Amount) where
75 type Amount_Unit (Map Unit Amount) = Unit
76 amount_add = Data.Map.unionWith (+)
78 -- | An 'Amount' is a partially valid 'Num' instance:
80 -- * (+) operator requires both operand to have the same 'Unit' (homogeneity constraint).
81 -- * (*) operator requires at least one operand with an empty 'Unit' ('scalar').
82 instance Num Amount where
83 abs a@Amount{quantity=q} = a{quantity=abs q}
84 fromInteger = scalar . fromInteger
85 negate a@Amount{quantity=q} = a{quantity=negate q}
86 signum a@Amount{quantity=q} = a{quantity=signum q}
88 let s@(Style.Style{Style.precision=p}) = Style.union (style a) (style b) in
89 a{ quantity = Quantity.round p $ quantity a + quantity b
94 else error "(+) on non-homogeneous units"
97 let Style.Style{Style.precision=p} = s in
98 a{ quantity = Quantity.round p $ quantity a * quantity b
105 then (Style.union (style a) (style b), "")
106 else (style b, unit b)
107 | unit b == "" = (style a, unit a)
108 | otherwise = error "(*) by non-scalar unit"
115 { quantity = Quantity.zero
120 -- *** From 'Quantity'
122 -- | Return an 'Amount' with 'unit' = Unit.'Unit.nil'.
123 scalar :: Quantity -> Amount
127 , style = Style.Style
128 { Style.fractioning = Just '.'
129 , Style.grouping_fractional = Just $ Style.Grouping ',' [3]
130 , Style.grouping_integral = Just $ Style.Grouping ',' [3]
131 , Style.precision = maxBound
132 , Style.unit_side = Just Style.Side_Right
133 , Style.unit_spaced = Just False
138 -- | <https://en.wikipedia.org/wiki/Swiss_franc Swiss franc> unit of currency.
139 chf :: Quantity -> Amount
143 , style = Style.Style
144 { Style.fractioning = Just ','
145 , Style.grouping_fractional = Just $ Style.Grouping '.' [3]
146 , Style.grouping_integral = Just $ Style.Grouping '.' [3]
147 , Style.precision = 2
148 , Style.unit_side = Just Style.Side_Right
149 , Style.unit_spaced = Just False
153 -- | <https://en.wikipedia.org/wiki/Yuan Yuan> unit of currency.
154 cny :: Quantity -> Amount
158 , style = Style.Style
159 { Style.fractioning = Just ','
160 , Style.grouping_fractional = Just $ Style.Grouping '.' [3]
161 , Style.grouping_integral = Just $ Style.Grouping '.' [3]
162 , Style.precision = 2
163 , Style.unit_side = Just Style.Side_Right
164 , Style.unit_spaced = Just False
168 -- | <https://en.wikipedia.org/wiki/Euro Euro> unit of currency.
169 eur :: Quantity -> Amount
173 , style = Style.Style
174 { Style.fractioning = Just ','
175 , Style.grouping_fractional = Just $ Style.Grouping '.' [3]
176 , Style.grouping_integral = Just $ Style.Grouping '.' [3]
177 , Style.precision = 2
178 , Style.unit_side = Just Style.Side_Right
179 , Style.unit_spaced = Just False
183 -- | <https://en.wikipedia.org/wiki/Pound_sterling Pound sterling> unit of currency.
184 gbp :: Quantity -> Amount
188 , style = Style.Style
189 { Style.fractioning = Just '.'
190 , Style.grouping_fractional = Just $ Style.Grouping ',' [3]
191 , Style.grouping_integral = Just $ Style.Grouping ',' [3]
192 , Style.precision = 2
193 , Style.unit_side = Just Style.Side_Left
194 , Style.unit_spaced = Just False
198 -- | <https://en.wikipedia.org/wiki/Indian_rupee Indian rupee> unit of currency.
199 inr :: Quantity -> Amount
203 , style = Style.Style
204 { Style.fractioning = Just ','
205 , Style.grouping_fractional = Just $ Style.Grouping '.' [3]
206 , Style.grouping_integral = Just $ Style.Grouping '.' [3]
207 , Style.precision = 2
208 , Style.unit_side = Just Style.Side_Right
209 , Style.unit_spaced = Just False
213 -- | <https://en.wikipedia.org/wiki/Japanese_yen Japanese yen> unit of currency.
214 jpy :: Quantity -> Amount
218 , style = Style.Style
219 { Style.fractioning = Just '.'
220 , Style.grouping_fractional = Just $ Style.Grouping ',' [3]
221 , Style.grouping_integral = Just $ Style.Grouping ',' [3]
222 , Style.precision = 2
223 , Style.unit_side = Just Style.Side_Left
224 , Style.unit_spaced = Just False
228 -- | <https://en.wikipedia.org/wiki/Russian_ruble Russian ruble> unit of currency.
230 -- NOTE: Ꝑ (U+A750) is used as a replacement latin letter,
231 -- because GHC currently chokes on ₽ (U+20BD),
232 -- which is the recently (2014/02) assigned Unicode code-point
233 -- for this currency.
234 rub :: Quantity -> Amount
238 , style = Style.Style
239 { Style.fractioning = Just '.'
240 , Style.grouping_fractional = Just $ Style.Grouping ',' [3]
241 , Style.grouping_integral = Just $ Style.Grouping ',' [3]
242 , Style.precision = 2
243 , Style.unit_side = Just Style.Side_Left
244 , Style.unit_spaced = Just False
248 -- | <https://en.wikipedia.org/wiki/United_States_dollar United States dollar> unit of currency.
249 usd :: Quantity -> Amount
253 , style = Style.Style
254 { Style.fractioning = Just '.'
255 , Style.grouping_fractional = Just $ Style.Grouping ',' [3]
256 , Style.grouping_integral = Just $ Style.Grouping ',' [3]
257 , Style.precision = 2
258 , Style.unit_side = Just Style.Side_Left
259 , Style.unit_spaced = Just False
266 -- | Return 'True' if and only if 'Amount'’s 'Quantity' is zero.
268 -- NOTE: the 'Amount'’s 'quantity' MUST be have been rounded at 'Amount'’s 'style'’s 'Style.precision'.
269 is_zero :: Amount -> Bool
270 is_zero = Quantity.is_zero . quantity
272 -- * The 'Amount_by_Unit' mapping
275 = Data.Map.Map Unit Amount
276 type By_Unit = Amount_by_Unit
278 -- | 'Amount_by_Unit'’s is a partially valid 'Num' instance.
280 -- * (*) operator is not defined.
281 instance Num Amount_by_Unit where
282 abs = Data.Map.map (\a@Amount{quantity=q} -> a{quantity=abs q})
283 fromInteger = Data.Map.singleton "" . fromInteger
284 negate = Data.Map.map (\a@Amount{quantity=q} -> a{quantity=negate q})
285 signum = Data.Map.map (\a@Amount{quantity=q} -> a{quantity=signum q})
286 (+) = Data.Map.unionWith (+)
287 (*) = error "(*) not-supported"
289 type Signs = (Int, Int)
291 signs :: Amount_by_Unit -> Signs
292 signs = Data.Map.foldl'
293 (\(nega, plus) amt ->
294 case flip compare 0 $ quantity amt of
295 LT -> (nega - 1, plus)
297 GT -> (nega, plus + 1))
302 nil_By_Unit :: Amount_by_Unit
308 -- | Return 'True' if and only if all 'Amount's satisfy 'is_zero'.
309 are_zero :: Amount_by_Unit -> Bool
310 are_zero = Data.Foldable.all is_zero
312 -- | Return a tuple associating the given 'Amount' with its 'Unit'.
313 assoc_by_unit :: Amount -> (Unit, Amount)
314 assoc_by_unit amount = (unit amount, amount)
316 -- | Return a 'Data.Map.Map' associating the given 'Amount's with their respective 'Unit'.
317 from_List :: [Amount] -> Amount_by_Unit
319 Data.Map.fromListWith (flip (+)) $ -- NOTE: flip to propagate 'Style' first to last.
320 Data.List.map assoc_by_unit amounts
324 -- ** Class 'Sumable'
326 ( Data (Sumable_Unit a)
329 , Ord (Sumable_Unit a)
330 , Show (Sumable_Unit a)
332 , Typeable (Sumable_Unit a)
336 -- sumable_add :: a -> a -> a
337 sumable_positive :: a -> Maybe a
338 sumable_negative :: a -> Maybe a
340 instance Sumable Amount where
341 type Sumable_Unit Amount = Unit
344 case compare (quantity a) Quantity.zero of
349 case compare (quantity a) Quantity.zero of
354 instance Sumable (Map Unit Amount) where
355 type Sumable_Unit (Map Unit Amount) = Unit
358 let r = Data.Map.mapMaybe sumable_positive a in
363 let r = Data.Map.mapMaybe sumable_negative a in
368 instance Sumable amount => Sumable (Sum amount) where
369 type Sumable_Unit (Sum amount) = Sumable_Unit amount
370 sumable_negative amt =
372 Sum_Negative _ -> Just $ amt
373 Sum_Positive _ -> Nothing
374 Sum_Both n _ -> Just $ Sum_Negative n
375 sumable_positive amt =
377 Sum_Negative _ -> Nothing
378 Sum_Positive _ -> Just $ amt
379 Sum_Both _ p -> Just $ Sum_Positive p
381 -- | Sum separately keeping track of negative and positive 'amount's.
383 = Sum_Negative !amount
384 | Sum_Positive !amount
385 | Sum_Both !amount !amount
386 deriving (Data, Eq, Show, Typeable)
387 instance NFData amount => NFData (Sum amount) where
388 rnf (Sum_Negative a) = rnf a
389 rnf (Sum_Positive a) = rnf a
390 rnf (Sum_Both a0 a1) = rnf a0 `seq` rnf a1
392 instance Functor Sum where
393 fmap f (Sum_Negative a) = Sum_Negative (f a)
394 fmap f (Sum_Positive a) = Sum_Positive (f a)
395 fmap f (Sum_Both a0 a1) = Sum_Both (f a0) (f a1)
397 instance Balance.Amount a
398 => Balance.Amount (Sum a) where
399 type Amount_Unit (Sum a) = Balance.Amount_Unit a
402 Sum_Negative n -> Balance.amount_null n
403 Sum_Positive p -> Balance.amount_null p
404 Sum_Both n p -> Balance.amount_null (Balance.amount_add n p)
407 (Sum_Negative n0, Sum_Negative n1) -> Sum_Negative (Balance.amount_add n0 n1)
408 (Sum_Negative n , Sum_Positive p) -> Sum_Both n p
409 (Sum_Negative n0, Sum_Both n1 p) -> Sum_Both (Balance.amount_add n0 n1) p
411 (Sum_Positive p , Sum_Negative n) -> Sum_Both n p
412 (Sum_Positive p0, Sum_Positive p1) -> Sum_Positive (Balance.amount_add p0 p1)
413 (Sum_Positive p , Sum_Both n1 p1) -> Sum_Both n1 (Balance.amount_add p p1)
415 (Sum_Both n0 p0, Sum_Negative n) -> Sum_Both (Balance.amount_add n0 n) p0
416 (Sum_Both n0 p0, Sum_Positive p1) -> Sum_Both n0 (Balance.amount_add p0 p1)
417 (Sum_Both n0 p0, Sum_Both n1 p1) -> Sum_Both (Balance.amount_add n0 n1) (Balance.amount_add p0 p1)
420 Sum_Negative n -> Sum_Positive $ Balance.amount_negate n
421 Sum_Positive p -> Sum_Negative $ Balance.amount_negate p
422 Sum_Both n p -> Sum_Both (Balance.amount_negate p) (Balance.amount_negate n)
424 instance GL.Amount (Sum (Map Unit Amount)) where
425 type Amount_Unit (Sum (Map Unit Amount)) = Unit
428 (Sum_Negative n0, Sum_Negative n1) -> Sum_Negative (GL.amount_add n0 n1)
429 (Sum_Negative n , Sum_Positive p) -> Sum_Both n p
430 (Sum_Negative n0, Sum_Both n1 p) -> Sum_Both (GL.amount_add n0 n1) p
432 (Sum_Positive p , Sum_Negative n) -> Sum_Both n p
433 (Sum_Positive p0, Sum_Positive p1) -> Sum_Positive (GL.amount_add p0 p1)
434 (Sum_Positive p , Sum_Both n1 p1) -> Sum_Both n1 (GL.amount_add p p1)
436 (Sum_Both n0 p0, Sum_Negative n) -> Sum_Both (GL.amount_add n0 n) p0
437 (Sum_Both n0 p0, Sum_Positive p1) -> Sum_Both n0 (GL.amount_add p0 p1)
438 (Sum_Both n0 p0, Sum_Both n1 p1) -> Sum_Both (GL.amount_add n0 n1) (GL.amount_add p0 p1)
442 => amount -> Sum amount
444 case ( sumable_negative amt
445 , sumable_positive amt ) of
446 (Just n, Nothing) -> Sum_Negative n
447 (Nothing, Just p) -> Sum_Positive p
448 (Just n, Just p) -> Sum_Both n p
449 (Nothing, Nothing) -> Sum_Both amt amt
452 :: Sum amount -> Maybe amount
455 Sum_Negative n -> Just n
456 Sum_Positive _ -> Nothing
457 Sum_Both n _ -> Just n
460 :: Sum amount -> Maybe amount
463 Sum_Negative _ -> Nothing
464 Sum_Positive p -> Just p
465 Sum_Both _ p -> Just p
469 => Sum amount -> amount
474 Sum_Both n p -> GL.amount_add n p