1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE OverloadedStrings #-}
4 {-# LANGUAGE TypeFamilies #-}
5 {-# LANGUAGE TypeSynonymInstances #-}
6 {-# OPTIONS_GHC -fno-warn-orphans #-}
7 module Hcompta.Model.Amount where
10 import qualified Data.List
11 import qualified Data.Map.Strict as Data.Map
12 import qualified Data.Foldable
13 import Data.Typeable ()
15 import qualified Hcompta.Calc.Balance as Balance
16 import qualified Hcompta.Model.Amount.Quantity as Quantity
17 import qualified Hcompta.Model.Amount.Style as Style
18 import qualified Hcompta.Model.Amount.Unit as Unit
20 -- * Type synonyms to submodules
22 type Quantity = Quantity.Quantity
23 type Style = Style.Style
26 -- * The 'Amount' type
30 { quantity :: Quantity
33 } deriving (Data, Show, Typeable)
35 instance Eq Amount where
37 Amount{quantity=q0, unit=u0}
38 Amount{quantity=q1, unit=u1} =
42 EQ -> (==) q0 q1 -- NOTE: MUST already be rounded to Style.precision
44 instance Ord Amount where
46 Amount{quantity=q0, unit=u0}
47 Amount{quantity=q1, unit=u1} =
51 EQ -> compare q0 q1 -- NOTE: MUST already be rounded to Style.precision
53 instance Balance.Amount Amount where
54 type Amount_Unit Amount = Unit
55 amount_null = (==) Quantity.zero . quantity
57 amount_negate = negate
59 case compare (quantity a) Quantity.zero of
64 case compare (quantity a) Quantity.zero of
69 -- | An 'Amount' is a partially valid 'Num' instance:
71 -- * (+) operator requires both operand to have the same 'Unit' (homogeneity constraint).
72 -- * (*) operator requires at least one operand with an empty 'Unit' ('scalar').
73 instance Num Amount where
74 abs a@Amount{quantity=q} = a{quantity=abs q}
75 fromInteger = scalar . fromInteger
76 negate a@Amount{quantity=q} = a{quantity=negate q}
77 signum a@Amount{quantity=q} = a{quantity=signum q}
79 let s@(Style.Style{Style.precision=p}) = Style.union (style a) (style b) in
80 a{ quantity = Quantity.round p $ quantity a + quantity b
85 else error "(+) on non-homogeneous units"
88 let Style.Style{Style.precision=p} = s in
89 a{ quantity = Quantity.round p $ quantity a * quantity b
97 then (Style.union (style a) (style b), "")
98 else (style b, unit b)
101 then (style a, unit a)
102 else error "(*) by non-scalar unit"
109 { quantity = Quantity.zero
114 -- *** From 'Quantity'
116 -- | Return an 'Amount' with 'unit' = Unit.'Unit.nil'.
117 scalar :: Quantity -> Amount
121 , style = Style.Style
122 { Style.fractioning = Just '.'
123 , Style.grouping_fractional = Just $ Style.Grouping ',' [3]
124 , Style.grouping_integral = Just $ Style.Grouping ',' [3]
125 , Style.precision = maxBound
126 , Style.unit_side = Just Style.Side_Right
127 , Style.unit_spaced = Just False
132 -- | <https://en.wikipedia.org/wiki/Swiss_franc Swiss franc> unit of currency.
133 chf :: Quantity -> Amount
137 , style = Style.Style
138 { Style.fractioning = Just ','
139 , Style.grouping_fractional = Just $ Style.Grouping '.' [3]
140 , Style.grouping_integral = Just $ Style.Grouping '.' [3]
141 , Style.precision = 2
142 , Style.unit_side = Just Style.Side_Right
143 , Style.unit_spaced = Just False
147 -- | <https://en.wikipedia.org/wiki/Yuan Yuan> unit of currency.
148 cny :: Quantity -> Amount
152 , style = Style.Style
153 { Style.fractioning = Just ','
154 , Style.grouping_fractional = Just $ Style.Grouping '.' [3]
155 , Style.grouping_integral = Just $ Style.Grouping '.' [3]
156 , Style.precision = 2
157 , Style.unit_side = Just Style.Side_Right
158 , Style.unit_spaced = Just False
162 -- | <https://en.wikipedia.org/wiki/Euro Euro> unit of currency.
163 eur :: Quantity -> Amount
167 , style = Style.Style
168 { Style.fractioning = Just ','
169 , Style.grouping_fractional = Just $ Style.Grouping '.' [3]
170 , Style.grouping_integral = Just $ Style.Grouping '.' [3]
171 , Style.precision = 2
172 , Style.unit_side = Just Style.Side_Right
173 , Style.unit_spaced = Just False
177 -- | <https://en.wikipedia.org/wiki/Pound_sterling Pound sterling> unit of currency.
178 gbp :: Quantity -> Amount
182 , style = Style.Style
183 { Style.fractioning = Just '.'
184 , Style.grouping_fractional = Just $ Style.Grouping ',' [3]
185 , Style.grouping_integral = Just $ Style.Grouping ',' [3]
186 , Style.precision = 2
187 , Style.unit_side = Just Style.Side_Left
188 , Style.unit_spaced = Just False
192 -- | <https://en.wikipedia.org/wiki/Indian_rupee Indian rupee> unit of currency.
193 inr :: Quantity -> Amount
197 , style = Style.Style
198 { Style.fractioning = Just ','
199 , Style.grouping_fractional = Just $ Style.Grouping '.' [3]
200 , Style.grouping_integral = Just $ Style.Grouping '.' [3]
201 , Style.precision = 2
202 , Style.unit_side = Just Style.Side_Right
203 , Style.unit_spaced = Just False
207 -- | <https://en.wikipedia.org/wiki/Japanese_yen Japanese yen> unit of currency.
208 jpy :: Quantity -> Amount
212 , style = Style.Style
213 { Style.fractioning = Just '.'
214 , Style.grouping_fractional = Just $ Style.Grouping ',' [3]
215 , Style.grouping_integral = Just $ Style.Grouping ',' [3]
216 , Style.precision = 2
217 , Style.unit_side = Just Style.Side_Left
218 , Style.unit_spaced = Just False
222 -- | <https://en.wikipedia.org/wiki/Russian_ruble Russian ruble> unit of currency.
224 -- NOTE: Ꝑ (U+A750) is used as a replacement latin letter,
225 -- because GHC currently chokes on ₽ (U+20BD),
226 -- which is the recently (2014/02) assigned Unicode code-point
227 -- for this currency.
228 rub :: Quantity -> Amount
232 , style = Style.Style
233 { Style.fractioning = Just '.'
234 , Style.grouping_fractional = Just $ Style.Grouping ',' [3]
235 , Style.grouping_integral = Just $ Style.Grouping ',' [3]
236 , Style.precision = 2
237 , Style.unit_side = Just Style.Side_Left
238 , Style.unit_spaced = Just False
242 -- | <https://en.wikipedia.org/wiki/United_States_dollar United States dollar> unit of currency.
243 usd :: Quantity -> Amount
247 , style = Style.Style
248 { Style.fractioning = Just '.'
249 , Style.grouping_fractional = Just $ Style.Grouping ',' [3]
250 , Style.grouping_integral = Just $ Style.Grouping ',' [3]
251 , Style.precision = 2
252 , Style.unit_side = Just Style.Side_Left
253 , Style.unit_spaced = Just False
260 -- | Return 'True' if and only if 'Amount'’s 'Quantity' is zero.
262 -- NOTE: the 'Amount'’s 'quantity' MUST be have been rounded at 'Amount'’s 'style'’s 'Style.precision'.
263 is_zero :: Amount -> Bool
264 is_zero = Quantity.is_zero . quantity
266 -- * The 'Amount_by_Unit' mapping
269 = Data.Map.Map Unit Amount
270 type By_Unit = Amount_by_Unit
272 -- | 'Amount_by_Unit'’s is a partially valid 'Num' instance.
274 -- * (*) operator is not defined.
275 instance Num Amount_by_Unit where
276 abs = Data.Map.map (\a@Amount{quantity=q} -> a{quantity=abs q})
277 fromInteger = Data.Map.singleton "" . fromInteger
278 negate = Data.Map.map (\a@Amount{quantity=q} -> a{quantity=negate q})
279 signum = Data.Map.map (\a@Amount{quantity=q} -> a{quantity=signum q})
280 (+) = Data.Map.unionWith (+)
281 (*) = error "(*) not-supported"
283 type Signs = (Int, Int)
285 signs :: Amount_by_Unit -> Signs
286 signs = Data.Map.foldl
287 (\(nega, plus) amt ->
288 case flip compare 0 $ quantity amt of
289 LT -> (nega - 1, plus)
291 GT -> (nega, plus + 1))
296 nil_By_Unit :: Amount_by_Unit
302 -- | Return 'True' if and only if all 'Amount's satisfy 'is_zero'.
303 are_zero :: Amount_by_Unit -> Bool
304 are_zero = Data.Foldable.all is_zero
306 -- | Return a tuple associating the given 'Amount' with its 'Unit'.
307 assoc_by_unit :: Amount -> (Unit, Amount)
308 assoc_by_unit amount = (unit amount, amount)
310 -- | Return a 'Data.Map.Map' associating the given 'Amount's with their respective 'Unit'.
311 from_List :: [Amount] -> Amount_by_Unit
313 Data.Map.fromListWith (flip (+)) $ -- NOTE: flip to propagate 'Style' first to last.
314 Data.List.map assoc_by_unit amounts