1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE FlexibleContexts #-}
3 {-# LANGUAGE FlexibleInstances #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE TypeFamilies #-}
6 {-# LANGUAGE TypeSynonymInstances #-}
7 {-# OPTIONS_GHC -fno-warn-orphans #-}
8 module Hcompta.Amount where
11 import qualified Data.List
12 import qualified Data.Map.Strict as Data.Map
13 import qualified Data.Foldable
14 import Data.Typeable ()
16 import qualified Hcompta.Balance as Balance
17 import qualified Hcompta.Amount.Quantity as Quantity
18 import qualified Hcompta.Amount.Style as Style
19 import qualified Hcompta.Amount.Unit as Unit
21 -- * Type synonyms to submodules
23 type Quantity = Quantity.Quantity
24 type Style = Style.Style
27 -- * The 'Amount' type
31 { quantity :: Quantity
34 } deriving (Data, Show, Typeable)
36 instance Eq Amount where
38 Amount{quantity=q0, unit=u0}
39 Amount{quantity=q1, unit=u1} =
43 EQ -> (==) q0 q1 -- NOTE: MUST already be rounded to Style.precision
45 instance Ord Amount where
47 Amount{quantity=q0, unit=u0}
48 Amount{quantity=q1, unit=u1} =
52 EQ -> compare q0 q1 -- NOTE: MUST already be rounded to Style.precision
54 instance Balance.Amount Amount where
55 type Amount_Unit Amount = Unit
56 amount_null = (==) Quantity.zero . quantity
58 amount_negate = negate
60 -- | An 'Amount' is a partially valid 'Num' instance:
62 -- * (+) operator requires both operand to have the same 'Unit' (homogeneity constraint).
63 -- * (*) operator requires at least one operand with an empty 'Unit' ('scalar').
64 instance Num Amount where
65 abs a@Amount{quantity=q} = a{quantity=abs q}
66 fromInteger = scalar . fromInteger
67 negate a@Amount{quantity=q} = a{quantity=negate q}
68 signum a@Amount{quantity=q} = a{quantity=signum q}
70 let s@(Style.Style{Style.precision=p}) = Style.union (style a) (style b) in
71 a{ quantity = Quantity.round p $ quantity a + quantity b
76 else error "(+) on non-homogeneous units"
79 let Style.Style{Style.precision=p} = s in
80 a{ quantity = Quantity.round p $ quantity a * quantity b
88 then (Style.union (style a) (style b), "")
89 else (style b, unit b)
92 then (style a, unit a)
93 else error "(*) by non-scalar unit"
100 { quantity = Quantity.zero
105 -- *** From 'Quantity'
107 -- | Return an 'Amount' with 'unit' = Unit.'Unit.nil'.
108 scalar :: Quantity -> Amount
112 , style = Style.Style
113 { Style.fractioning = Just '.'
114 , Style.grouping_fractional = Just $ Style.Grouping ',' [3]
115 , Style.grouping_integral = Just $ Style.Grouping ',' [3]
116 , Style.precision = maxBound
117 , Style.unit_side = Just Style.Side_Right
118 , Style.unit_spaced = Just False
123 -- | <https://en.wikipedia.org/wiki/Swiss_franc Swiss franc> unit of currency.
124 chf :: Quantity -> Amount
128 , style = Style.Style
129 { Style.fractioning = Just ','
130 , Style.grouping_fractional = Just $ Style.Grouping '.' [3]
131 , Style.grouping_integral = Just $ Style.Grouping '.' [3]
132 , Style.precision = 2
133 , Style.unit_side = Just Style.Side_Right
134 , Style.unit_spaced = Just False
138 -- | <https://en.wikipedia.org/wiki/Yuan Yuan> unit of currency.
139 cny :: 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/Euro Euro> unit of currency.
154 eur :: 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/Pound_sterling Pound sterling> unit of currency.
169 gbp :: 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_Left
179 , Style.unit_spaced = Just False
183 -- | <https://en.wikipedia.org/wiki/Indian_rupee Indian rupee> unit of currency.
184 inr :: 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_Right
194 , Style.unit_spaced = Just False
198 -- | <https://en.wikipedia.org/wiki/Japanese_yen Japanese yen> unit of currency.
199 jpy :: 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_Left
209 , Style.unit_spaced = Just False
213 -- | <https://en.wikipedia.org/wiki/Russian_ruble Russian ruble> unit of currency.
215 -- NOTE: Ꝑ (U+A750) is used as a replacement latin letter,
216 -- because GHC currently chokes on ₽ (U+20BD),
217 -- which is the recently (2014/02) assigned Unicode code-point
218 -- for this currency.
219 rub :: Quantity -> Amount
223 , style = Style.Style
224 { Style.fractioning = Just '.'
225 , Style.grouping_fractional = Just $ Style.Grouping ',' [3]
226 , Style.grouping_integral = Just $ Style.Grouping ',' [3]
227 , Style.precision = 2
228 , Style.unit_side = Just Style.Side_Left
229 , Style.unit_spaced = Just False
233 -- | <https://en.wikipedia.org/wiki/United_States_dollar United States dollar> unit of currency.
234 usd :: 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
251 -- | Return 'True' if and only if 'Amount'’s 'Quantity' is zero.
253 -- NOTE: the 'Amount'’s 'quantity' MUST be have been rounded at 'Amount'’s 'style'’s 'Style.precision'.
254 is_zero :: Amount -> Bool
255 is_zero = Quantity.is_zero . quantity
257 -- * The 'Amount_by_Unit' mapping
260 = Data.Map.Map Unit Amount
261 type By_Unit = Amount_by_Unit
263 -- | 'Amount_by_Unit'’s is a partially valid 'Num' instance.
265 -- * (*) operator is not defined.
266 instance Num Amount_by_Unit where
267 abs = Data.Map.map (\a@Amount{quantity=q} -> a{quantity=abs q})
268 fromInteger = Data.Map.singleton "" . fromInteger
269 negate = Data.Map.map (\a@Amount{quantity=q} -> a{quantity=negate q})
270 signum = Data.Map.map (\a@Amount{quantity=q} -> a{quantity=signum q})
271 (+) = Data.Map.unionWith (+)
272 (*) = error "(*) not-supported"
274 type Signs = (Int, Int)
276 signs :: Amount_by_Unit -> Signs
277 signs = Data.Map.foldl
278 (\(nega, plus) amt ->
279 case flip compare 0 $ quantity amt of
280 LT -> (nega - 1, plus)
282 GT -> (nega, plus + 1))
287 nil_By_Unit :: Amount_by_Unit
293 -- | Return 'True' if and only if all 'Amount's satisfy 'is_zero'.
294 are_zero :: Amount_by_Unit -> Bool
295 are_zero = Data.Foldable.all is_zero
297 -- | Return a tuple associating the given 'Amount' with its 'Unit'.
298 assoc_by_unit :: Amount -> (Unit, Amount)
299 assoc_by_unit amount = (unit amount, amount)
301 -- | Return a 'Data.Map.Map' associating the given 'Amount's with their respective 'Unit'.
302 from_List :: [Amount] -> Amount_by_Unit
304 Data.Map.fromListWith (flip (+)) $ -- NOTE: flip to propagate 'Style' first to last.
305 Data.List.map assoc_by_unit amounts
309 -- ** Class 'Sumable'
311 ( Data (Sumable_Unit a)
314 , Ord (Sumable_Unit a)
315 , Show (Sumable_Unit a)
317 , Typeable (Sumable_Unit a)
321 -- sumable_add :: a -> a -> a
322 sumable_positive :: a -> Maybe a
323 sumable_negative :: a -> Maybe a
325 instance Sumable Amount where
326 type Sumable_Unit Amount = Unit
329 case compare (quantity a) Quantity.zero of
334 case compare (quantity a) Quantity.zero of
339 instance Sumable amount => Sumable (Sum amount) where
340 type Sumable_Unit (Sum amount) = Sumable_Unit amount
341 sumable_negative amt =
343 Sum_Negative _ -> Just $ amt
344 Sum_Positive _ -> Nothing
345 Sum_Both n _ -> Just $ Sum_Negative n
346 sumable_positive amt =
348 Sum_Negative _ -> Nothing
349 Sum_Positive _ -> Just $ amt
350 Sum_Both _ p -> Just $ Sum_Positive p
352 -- | Sum separately keeping track of negative and positive 'amount's.
354 = Sum_Negative amount
355 | Sum_Positive amount
356 | Sum_Both amount amount
357 deriving (Data, Eq, Show, Typeable)
359 instance Balance.Amount a
360 => Balance.Amount (Sum a) where
361 type Amount_Unit (Sum a) = Balance.Amount_Unit a
364 Sum_Negative n -> Balance.amount_null n
365 Sum_Positive p -> Balance.amount_null p
366 Sum_Both n p -> Balance.amount_null (Balance.amount_add n p)
369 (Sum_Negative n0, Sum_Negative n1) -> Sum_Negative (Balance.amount_add n0 n1)
370 (Sum_Negative n , Sum_Positive p) -> Sum_Both n p
371 (Sum_Negative n0, Sum_Both n1 p) -> Sum_Both (Balance.amount_add n0 n1) p
373 (Sum_Positive p , Sum_Negative n) -> Sum_Both n p
374 (Sum_Positive p0, Sum_Positive p1) -> Sum_Positive (Balance.amount_add p0 p1)
375 (Sum_Positive p , Sum_Both n1 p1) -> Sum_Both n1 (Balance.amount_add p p1)
377 (Sum_Both n0 p0, Sum_Negative p1) -> Sum_Both n0 (Balance.amount_add p0 p1)
378 (Sum_Both n0 p0, Sum_Positive p1) -> Sum_Both n0 (Balance.amount_add p0 p1)
379 (Sum_Both n0 p0, Sum_Both n1 p1) -> Sum_Both (Balance.amount_add n0 n1) (Balance.amount_add p0 p1)
382 Sum_Negative n -> Sum_Positive $ Balance.amount_negate n
383 Sum_Positive p -> Sum_Negative $ Balance.amount_negate p
384 Sum_Both n p -> Sum_Both (Balance.amount_negate p) (Balance.amount_negate n)
388 => amount -> Sum amount
390 case ( sumable_negative amt
391 , sumable_positive amt ) of
392 (Just n, Nothing) -> Sum_Negative n
393 (Nothing, Just p) -> Sum_Positive p
394 (Just n, Just p) -> Sum_Both n p
395 (Nothing, Nothing) -> Sum_Both amt amt
398 :: Balance.Amount amount
399 => Sum amount -> Maybe amount
402 Sum_Negative n -> Just n
403 Sum_Positive _ -> Nothing
404 Sum_Both n _ -> Just n
407 :: Balance.Amount amount
408 => Sum amount -> Maybe amount
411 Sum_Negative _ -> Nothing
412 Sum_Positive p -> Just p
413 Sum_Both _ p -> Just p
416 :: Balance.Amount amount
417 => Sum amount -> amount
422 Sum_Both n p -> Balance.amount_add n p