1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE MagicHash #-}
4 {-# LANGUAGE NamedFieldPuns #-}
5 {-# LANGUAGE OverloadedStrings #-}
6 {-# LANGUAGE ScopedTypeVariables #-}
7 module Hcompta.Format.Ledger.Write where
9 import Control.Applicative ((<$>), (<*))
10 -- import Control.Arrow ((***))
11 import Data.Decimal (DecimalRaw(..))
12 import qualified Data.Char (isSpace)
13 import Data.Fixed (showFixed)
14 import qualified Data.Functor.Compose
15 import qualified Data.Foldable
16 import Data.Foldable (Foldable)
17 import qualified Data.List
18 import qualified Data.List.NonEmpty
19 import qualified Data.Map.Strict as Data.Map
20 import Data.Maybe (fromMaybe)
21 import qualified Data.Text.Lazy as TL
22 import qualified Data.Text as Text
23 import qualified Data.Time.Calendar as Time (toGregorian)
24 import qualified Data.Time.LocalTime as Time
25 import qualified Hcompta.Lib.Leijen as W
26 import Hcompta.Lib.Leijen (Doc, (<>))
27 import System.IO (Handle)
28 import qualified Text.Parsec as R hiding (satisfy, char)
29 import Text.Parsec (Stream, ParsecT)
30 import GHC.Exts (Int(..))
31 import GHC.Integer.Logarithms (integerLogBase#)
33 import qualified Hcompta.Account as Account
34 import Hcompta.Account (Account)
35 import qualified Hcompta.Amount as Amount
36 import Hcompta.Amount (Amount)
37 import qualified Hcompta.Amount.Quantity as Quantity
38 import Hcompta.Amount.Quantity (Quantity)
39 import qualified Hcompta.Amount.Style as Amount.Style
40 import qualified Hcompta.Amount.Unit as Unit
41 import Hcompta.Amount.Unit (Unit)
42 import qualified Hcompta.Format.Ledger as Ledger
43 import Hcompta.Format.Ledger
46 , Posting(..), Posting_by_Account, Posting_Type(..)
50 -- import qualified Hcompta.Date as Date
51 import Hcompta.Date (Date)
52 -- import Hcompta.Format.Ledger.Journal as Journal
53 import qualified Hcompta.Format.Ledger.Read as Read
54 import qualified Hcompta.Lib.Parsec as R
57 -- * Printing 'Account'
59 account :: Posting_Type -> Account -> Doc
62 Posting_Type_Regular -> account_
63 Posting_Type_Virtual -> \acct ->
64 W.char Read.posting_type_virtual_begin <> do
66 W.char Read.posting_type_virtual_end
67 Posting_Type_Virtual_Balanced -> \acct ->
68 W.char Read.posting_type_virtual_balanced_begin <> do
70 W.char Read.posting_type_virtual_balanced_end
72 account_ :: Account -> Doc
75 Data.List.NonEmpty.toList $
76 Data.List.NonEmpty.intersperse
77 (W.bold $ W.yellow $ W.char Read.account_name_sep)
78 (Data.List.NonEmpty.map account_name acct)
80 account_name :: Account.Name -> Doc
81 account_name = W.strict_text
83 -- ** Mesuring 'Account'
85 account_length :: Posting_Type -> Account -> Int
86 account_length type_ acct =
88 (\acc -> (1 +) . (acc +) . Text.length)
91 Posting_Type_Regular -> 0
92 Posting_Type_Virtual -> 2
93 Posting_Type_Virtual_Balanced -> 2
95 -- * Printing 'Amount'
97 amount :: Amount -> Doc
100 , Amount.style = sty@(Amount.Style.Style
101 { Amount.Style.unit_side
102 , Amount.Style.unit_spaced
107 Just Amount.Style.Side_Left ->
109 <> (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
113 (Just Amount.Style.Side_Right) ->
114 (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
117 (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
122 unit = W.yellow . W.strict_text . Unit.text
124 quantity :: Amount.Style -> Quantity -> Doc
125 quantity Amount.Style.Style
126 { Amount.Style.fractioning
127 , Amount.Style.grouping_integral
128 , Amount.Style.grouping_fractional
129 , Amount.Style.precision
131 let Decimal e n = Quantity.round precision qty
132 let num = Prelude.show $ abs $ n
133 let sign = W.bold $ W.yellow $ W.strict_text (if n < 0 then "-" else "")
134 case e == 0 || precision == 0 of
135 True -> sign <> do W.bold $ W.blue $ (W.strict_text $ Text.pack num)
137 let num_len = length num
140 [ replicate (fromIntegral e + 1 - num_len) '0'
142 , replicate (fromIntegral precision - fromIntegral e) '0'
144 let (int, frac) = Data.List.splitAt (max 1 (num_len - fromIntegral precision)) padded
145 let default_fractioning =
147 del_grouping_sep grouping_integral $
148 del_grouping_sep grouping_fractional $
152 W.text (TL.pack $ maybe id (\g -> reverse . group g . reverse) grouping_integral $ int) <> do
153 (W.yellow $ W.char (fromMaybe default_fractioning fractioning)) <> do
154 W.text (TL.pack $ maybe id group grouping_fractional frac)
156 group :: Amount.Style.Grouping -> [Char] -> [Char]
157 group (Amount.Style.Grouping sep sizes_) =
158 Data.List.concat . reverse .
159 Data.List.map reverse . fst .
161 (flip (\digit -> \x -> case x of
162 ([], sizes) -> ([[digit]], sizes)
163 (digits:groups, []) -> ((digit:digits):groups, [])
164 (digits:groups, curr_sizes@(size:sizes)) ->
165 if length digits < size
166 then ( (digit:digits):groups, curr_sizes)
167 else ([digit]:[sep]:digits:groups, if sizes == [] then curr_sizes else sizes)
170 del_grouping_sep grouping =
172 Just (Amount.Style.Grouping sep _) -> Data.List.delete sep
175 -- ** Mesuring 'Amount'
177 amount_length :: Amount -> Int
178 amount_length Amount.Amount
179 { Amount.quantity = qty
180 , Amount.style = sty@(Amount.Style.Style
181 { Amount.Style.unit_spaced
183 , Amount.unit = unit_
186 + (case unit_spaced of { Just True | unit_ /= "" -> 1; _ -> 0 })
187 + quantity_length sty qty
189 amounts_length :: Amount.By_Unit -> Int
190 amounts_length amts =
191 if Data.Map.null amts
195 (\n -> (3 +) . (+) (amount_length n))
198 quantity_length :: Amount.Style -> Quantity -> Int
199 quantity_length Amount.Style.Style
200 { Amount.Style.grouping_integral
201 , Amount.Style.grouping_fractional
202 , Amount.Style.precision
204 let Decimal e n = Quantity.round precision qty in
205 let sign_len = if n < 0 then 1 else 0 in
206 let fractioning_len = if e > 0 then 1 else 0 in
207 let num_len = if n == 0 then 0 else (1 +) $ I# (integerLogBase# 10 (abs n)) in
208 let pad_left_len = max 0 (fromIntegral e + 1 - num_len) in
209 let pad_right_len = max 0 (fromIntegral precision - fromIntegral e) in
210 let padded_len = pad_left_len + num_len + pad_right_len in
211 let int_len = max 1 (num_len - fromIntegral precision) in
212 let frac_len = max 0 (padded_len - int_len) in
216 + maybe 0 (group int_len) grouping_integral
217 + maybe 0 (group frac_len) grouping_fractional
220 group :: Int -> Amount.Style.Grouping -> Int
221 group num_len (Amount.Style.Grouping _sep sizes_) =
224 else loop 0 num_len sizes_
226 loop :: Int -> Int -> [Int] -> Int
231 let l = len - size in
233 else loop (pad + 1) l sizes
235 let l = len - size in
237 else loop (pad + 1) l sizes
243 let (y, mo, d) = Time.toGregorian day
244 (if y == 0 then W.empty else W.integer y <> sep '/') <> do
246 sep '/' <> int2 d <> do
248 Time.TimeOfDay 0 0 0 -> W.empty
249 Time.TimeOfDay h m s ->
250 W.space <> int2 h <> do
251 sep ':' <> int2 m <> do
255 (if s < 10 then W.char '0' else W.empty) <> do
256 W.strict_text $ Text.pack $ showFixed True s)) <> do
259 _ | tz_name /= "" -> W.space <> do W.strict_text $ Text.pack tz_name
260 _ -> W.space <> do W.strict_text $ Text.pack $ Time.timeZoneOffsetString tz)
263 (Time.LocalTime day tod)
264 tz@(Time.TimeZone tz_min _ tz_name) =
265 Time.utcToZonedTime Time.utc utc
267 int2 i = if i < 10 then W.char '0' <> W.int i else W.int i
269 sep = W.bold . W.dullblack . W.char
271 -- * Printing 'Comment'
273 comment :: Comment -> Doc
276 W.char Read.comment_begin
277 <> (case Text.uncons com of
278 Just (c, _) | not $ Data.Char.isSpace c -> W.space
280 <> do W.if_color colorize (W.strict_text com)
285 pre <- R.many $ R.try $ do
286 ns <- R.many $ R.satisfy (\c -> c /= Read.tag_value_sep && not (Data.Char.isSpace c))
287 sh <- R.space_horizontal
289 ((W.text $ TL.pack $ concat pre) <>) <$> tags <* R.eof)
291 Left _ -> W.strict_text com
293 tags :: Stream s m Char => ParsecT s u m Doc
296 xs <- W.hcat <$> R.many (R.try (tag_sep >>= (\s -> (s <>) <$> tag_)))
299 tag_sep :: Stream s m Char => ParsecT s u m Doc
301 s <- R.char Read.tag_sep
302 sh <- R.many R.space_horizontal
304 do W.bold $ W.dullblack $ W.char s
305 <> do W.text $ TL.pack sh
306 tag_ :: Stream s m Char => ParsecT s u m Doc
309 s <- R.char Read.tag_value_sep
312 (W.yellow $ W.strict_text n)
313 <> (W.bold $ W.dullblack $ W.char s)
314 <> (W.red $ W.strict_text v)
316 comments :: Doc -> [Comment] -> Doc
319 Data.List.intersperse W.line .
320 Data.List.map (\c -> prefix <> comment c)
326 (W.dullyellow $ W.strict_text n)
327 <> W.char Read.tag_value_sep
328 <> (W.dullred $ W.strict_text v)
330 -- * Printing 'Posting'
332 posting :: Posting_Lengths -> Posting_Type -> Posting -> Doc
333 posting max_posting_length type_
335 { posting_account=acct
337 , posting_comments=cmts
339 , posting_status=status_
344 case Data.Map.null posting_amounts of
345 True -> account type_ acct
347 let len_acct = account_length type_ acct in
348 let len_amts = amounts_length posting_amounts in
349 account type_ acct <> do
350 W.fill (2 + max_posting_length - (len_acct + len_amts)) (W.space <> W.space) <> do
352 (W.space <> (W.bold $ W.yellow $ W.char Read.amount_sep) <> W.space)
353 amount posting_amounts
356 [c] -> W.space <> comment c
357 _ -> W.line <> do comments (W.text "\t ") cmts)
359 status :: Ledger.Status -> Doc
360 status = \x -> case x of
364 -- ** Mesuring 'Posting'
366 type Posting_Lengths = (Int)
370 -> Posting_by_Account
373 postings_lengths type_ ps pl =
377 ( account_length type_ (posting_account p)
378 + amounts_length (posting_amounts p) )
380 (Data.Functor.Compose.Compose ps)
382 -- * Printing 'Transaction'
384 transaction :: Transaction -> Doc
385 transaction t = transaction_with_lengths (transaction_lengths t 0) t
387 transactions :: Foldable f => f Transaction -> Doc
389 let transaction_lengths_ =
390 Data.Foldable.foldr transaction_lengths 0 ts
393 transaction_with_lengths transaction_lengths_ t <> W.line <>
394 (if W.is_empty doc then W.empty else W.line <> doc)
399 transaction_with_lengths :: Transaction_Lengths -> Transaction -> Doc
400 transaction_with_lengths
403 { transaction_code=code_
404 , transaction_comments_before
405 , transaction_comments_after
406 , transaction_dates=(first_date, dates)
407 , transaction_description
408 , transaction_postings
409 , transaction_virtual_postings
410 , transaction_balanced_virtual_postings
411 , transaction_status=status_
412 -- , transaction_tags
414 (case transaction_comments_before of
416 _ -> comments W.space transaction_comments_before <> W.line) <> do
418 Data.List.intersperse
419 (W.char Read.date_sep)
420 (Data.List.map date (first_date:dates))) <> do
422 True -> W.space <> status status_
423 False -> W.empty) <> do
425 (case transaction_description of
427 _ -> W.space <> (W.dullmagenta $ W.strict_text transaction_description)) <> do
429 (case transaction_comments_after of
431 _ -> comments W.space transaction_comments_after <> W.line) <> do
432 W.vsep $ Data.List.map
435 (W.intercalate W.line
436 (W.vsep . Data.List.map
437 (posting posting_lengths_ type_)))
438 (Ledger.posting_by_Signs_and_Account ps))
439 [ (Posting_Type_Regular, transaction_postings)
440 , (Posting_Type_Virtual, transaction_virtual_postings)
441 , (Posting_Type_Virtual_Balanced, transaction_balanced_virtual_postings)
444 code :: Ledger.Code -> Doc
445 code = \x -> case x of
447 t -> W.space <> W.char '(' <> W.strict_text t <> W.char ')'
449 -- ** Mesuring 'Transaction'
451 type Transaction_Lengths = Posting_Lengths
453 transaction_lengths :: Transaction -> Posting_Lengths -> Posting_Lengths
456 { transaction_postings
457 , transaction_virtual_postings
458 , transaction_balanced_virtual_postings
459 } posting_lengths_ = do
461 (flip (\(type_, ps) -> postings_lengths type_ ps))
463 [ (Posting_Type_Regular, transaction_postings)
464 , (Posting_Type_Virtual, transaction_virtual_postings)
465 , (Posting_Type_Virtual_Balanced, transaction_balanced_virtual_postings)
468 -- * Printing 'Journal'
470 journal :: Journal -> Doc
471 journal Journal { journal_transactions } =
472 transactions (Data.Functor.Compose.Compose journal_transactions)
478 { style_align :: Bool
479 , style_color :: Bool
488 show :: Style -> Doc -> TL.Text
489 show Style{style_color, style_align} =
492 then W.renderPretty style_color 1.0 maxBound
493 else W.renderCompact style_color
495 put :: Style -> Handle -> Doc -> IO ()
496 put Style{style_color, style_align} handle =
499 then W.renderPretty style_color 1.0 maxBound
500 else W.renderCompact style_color