1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE LambdaCase #-}
3 {-# LANGUAGE NamedFieldPuns #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE ScopedTypeVariables #-}
6 module Hcompta.Format.Ledger.Write where
8 import Control.Applicative ((<$>), (<*))
9 import Control.Arrow ((***))
10 import Data.Decimal (DecimalRaw(..))
11 import qualified Data.Char (isSpace)
12 import Data.Fixed (showFixed)
13 import qualified Data.List
14 import qualified Data.Map.Strict as Data.Map
15 import Data.Maybe (fromMaybe)
16 import qualified Data.Text.Lazy as TL
17 import qualified Data.Text as Text
18 import Data.Text (Text)
19 import qualified Data.Time.Calendar as Time (toGregorian)
20 import qualified Data.Time.LocalTime as Time (LocalTime(..), TimeOfDay(..), TimeZone(..), timeZoneOffsetString, ZonedTime(..))
21 -- import qualified Text.PrettyPrint.Leijen.Text as W
22 -- import Text.PrettyPrint.Leijen.Text (Doc, (<>))
23 import qualified Hcompta.Format.Text as W
24 import Hcompta.Format.Text (Doc, (<>))
25 import System.IO (Handle)
26 import qualified Text.Parsec as R
27 import Text.Parsec (Stream, ParsecT)
29 import qualified Hcompta.Model.Account as Account
30 import Hcompta.Model.Account (Account)
31 import qualified Hcompta.Model.Amount as Amount
32 import Hcompta.Model.Amount (Amount)
33 import qualified Hcompta.Model.Amount.Quantity as Quantity
34 import Hcompta.Model.Amount.Quantity (Quantity)
35 import qualified Hcompta.Model.Amount.Style as Style
36 import Hcompta.Model.Amount.Style (Style)
37 -- import qualified Hcompta.Model.Amount.Unit as Unit
38 import Hcompta.Model.Amount.Unit (Unit)
39 import qualified Hcompta.Model.Transaction as Transaction
40 import Hcompta.Model.Transaction (Comment, Tag, Transaction)
41 import qualified Hcompta.Model.Transaction.Posting as Posting
42 import Hcompta.Model.Transaction (Posting)
43 import qualified Hcompta.Model.Journal as Journal
44 import Hcompta.Model.Journal (Journal)
45 -- import qualified Hcompta.Model.Transaction.Tag as Tag
46 -- import Hcompta.Model.Transaction (Tag)
47 -- import qualified Hcompta.Model.Date as Date
48 import Hcompta.Model.Date (Date)
49 -- import Hcompta.Format.Ledger.Journal as Journal
50 import qualified Hcompta.Format.Ledger.Read as Read
56 show :: Bool -> Doc -> TL.Text
57 show with_color = W.displayT . W.renderPretty with_color 1.0 maxBound
59 showIO :: Bool -> Handle -> Doc -> IO ()
60 showIO with_color handle = W.displayIO handle . W.renderPretty with_color 1.0 maxBound
64 -- | Return a 'Doc' from a strict 'Text'
66 text = W.text . TL.fromStrict
68 -- | Return a 'Doc' concatenating converted values of a 'Map'
69 -- separated by a given 'Doc'
72 -> Data.Map.Map k a -> Doc
75 (\doc x -> doc <> (if W.is_empty doc then W.empty else sep) <> f x)
78 -- * Printing 'Account'
80 account :: Posting.Type -> Account -> Doc
83 Posting.Type_Regular -> account_
84 Posting.Type_Virtual -> \acct ->
85 W.char Read.posting_type_virtual_begin <> do
87 W.char Read.posting_type_virtual_end
88 Posting.Type_Virtual_Balanced -> \acct ->
89 W.char Read.posting_type_virtual_balanced_begin <> do
91 W.char Read.posting_type_virtual_balanced_end
93 account_ :: Account -> Doc
97 (W.bold $ W.dullblack $ W.char Read.account_name_sep)
98 (Data.List.map account_name acct)
100 account_name :: Account.Name -> Doc
103 -- ** Mesuring 'Account'
105 account_length :: Posting.Type -> Account -> Int
106 account_length type_ acct =
108 (\acc -> (1 +) . (acc +) . Text.length)
109 (if acct == [] then 0 else (- 1)) acct +
111 Posting.Type_Regular -> 0
112 Posting.Type_Virtual -> 2
113 Posting.Type_Virtual_Balanced -> 2
115 -- * Printing 'Amount'
117 amount :: Amount -> Doc
119 { Amount.quantity=qty
120 , Amount.style = style@(Style.Style
127 Just Style.Side_Left ->
129 <> (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
131 <> quantity style qty
133 (Just Style.Side_Right) ->
134 (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
137 (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
142 unit = W.yellow . text
144 quantity :: Style -> Quantity -> Doc
147 , Style.grouping_integral
148 , Style.grouping_fractional
151 let Decimal e n = Quantity.round precision qty
152 let num = Prelude.show $ abs $ n
153 let sign = W.bold $ W.yellow $ text (if n < 0 then "-" else "")
154 case e == 0 || precision == 0 of
155 True -> sign <> do W.bold $ W.blue $ (text $ Text.pack num)
157 let num_len = length num
160 [ replicate (fromIntegral e + 1 - num_len) '0'
162 , replicate (fromIntegral precision - fromIntegral e) '0'
164 let (int, frac) = Data.List.splitAt (max 1 (num_len - fromIntegral precision)) padded
165 let default_fractioning =
167 del_grouping_sep grouping_integral $
168 del_grouping_sep grouping_fractional $
172 W.text (TL.pack $ maybe id (\g -> reverse . group g . reverse) grouping_integral $ int) <> do
173 W.char (fromMaybe default_fractioning fractioning) <> do
174 W.text (TL.pack $ maybe id group grouping_fractional frac)
176 group :: Style.Grouping -> [Char] -> [Char]
177 group (Style.Grouping sep sizes_) =
178 Data.List.concat . reverse .
179 Data.List.map reverse . fst .
181 (flip (\digit -> \case
182 ([], sizes) -> ([[digit]], sizes)
183 (digits:groups, []) -> ((digit:digits):groups, [])
184 (digits:groups, curr_sizes@(size:sizes)) ->
185 if length digits < size
186 then ( (digit:digits):groups, curr_sizes)
187 else ([digit]:[sep]:digits:groups, if sizes == [] then curr_sizes else sizes)
190 del_grouping_sep grouping =
192 Just (Style.Grouping sep _) -> Data.List.delete sep
195 -- ** Mesuring 'Amount'
197 amount_length :: Amount -> Int
198 amount_length Amount.Amount
199 { Amount.quantity=qty
200 , Amount.style = style@(Style.Style
206 + (case unit_spaced of { Just True | unit_ /= "" -> 1; _ -> 0 })
207 + quantity_length style qty
209 amounts_length :: Amount.By_Unit -> Int
210 amounts_length amts =
211 if Data.Map.null amts
215 (\n -> (3 +) . (+) (amount_length n))
218 quantity_length :: Style -> Quantity -> Int
219 quantity_length Style.Style
220 { Style.grouping_integral
221 , Style.grouping_fractional
224 let Decimal e n = Quantity.round precision qty in
225 let sign_len = if n < 0 then 1 else 0 in
226 let fractioning_len = if e > 0 then 1 else 0 in
227 let num_len = if n == 0 then 0 else (1 +) $ truncate $ logBase 10 $ (fromIntegral (abs n)::Double) in
228 let pad_left_len = max 0 (fromIntegral e + 1 - num_len) in
229 let pad_right_len = max 0 (fromIntegral precision - fromIntegral e) in
230 let padded_len = pad_left_len + num_len + pad_right_len in
231 let int_len = max 1 (num_len - fromIntegral precision) in
232 let frac_len = max 0 (padded_len - int_len) in
236 + maybe 0 (group int_len) grouping_integral
237 + maybe 0 (group frac_len) grouping_fractional
240 group :: Int -> Style.Grouping -> Int
241 group num_len (Style.Grouping _sep sizes_) =
244 else loop 0 num_len sizes_
246 loop :: Int -> Int -> [Int] -> Int
251 let l = len - size in
253 else loop (pad + 1) l sizes
255 let l = len - size in
257 else loop (pad + 1) l sizes
263 (Time.LocalTime day tod)
264 tz@(Time.TimeZone tz_min _ tz_name)) = do
265 let (y, mo, d) = Time.toGregorian day
266 (if y == 0 then W.empty else W.integer y <> sep '/') <> do
268 sep '/' <> int2 d <> do
270 Time.TimeOfDay 0 0 0 -> W.empty
271 Time.TimeOfDay h m s ->
272 W.space <> int2 h <> do
273 sep ':' <> int2 m <> do
277 (if s < 10 then W.char '0' else W.empty) <> do
278 text $ Text.pack $ showFixed True s)) <> do
281 _ | tz_name /= "" -> W.space <> do text $ Text.pack tz_name
282 _ -> W.space <> do text $ Text.pack $ Time.timeZoneOffsetString tz)
285 int2 i = if i < 10 then W.char '0' <> W.int i else W.int i
287 sep = W.bold . W.dullblack . W.char
289 -- * Printing 'Comment'
291 comment :: Comment -> Doc
294 W.char Read.comment_begin
295 <> (case Text.uncons com of
296 Just (c, _) | not $ Data.Char.isSpace c -> W.space
298 <> do W.if_color colorize (text com)
303 pre <- R.many $ R.try $ do
304 ns <- R.many $ R.satisfy (\c -> c /= Read.tag_value_sep && not (Data.Char.isSpace c))
305 sh <- Read.space_horizontal
307 ((W.text $ TL.pack $ concat pre) <>) <$> tags <* R.eof)
311 tags :: Stream s m Char => ParsecT s u m Doc
314 xs <- W.hcat <$> R.many (R.try (tag_sep >>= (\s -> (s <>) <$> tag_)))
317 tag_sep :: Stream s m Char => ParsecT s u m Doc
319 s <- R.char Read.tag_sep
320 sh <- R.many Read.space_horizontal
322 do W.bold $ W.dullblack $ W.char s
323 <> do W.text $ TL.pack sh
324 tag_ :: Stream s m Char => ParsecT s u m Doc
327 s <- R.char Read.tag_value_sep
331 <> (W.bold $ W.dullblack $ W.char s)
334 comments :: Doc -> [Comment] -> Doc
337 Data.List.intersperse W.line .
338 Data.List.map (\c -> prefix <> comment c)
344 (W.dullyellow $ text n)
345 <> W.char Read.tag_value_sep
346 <> (W.dullred $ text v)
348 -- * Printing 'Posting'
350 posting :: Posting_Lengths -> Posting.Type -> Posting -> Doc
357 { Posting.account=acct
359 , Posting.comments=cmts
361 , Posting.status=status_
366 (case Data.Map.null amounts of
367 True -> account type_ acct
369 W.fill (max_account_length + 2)
370 (account type_ acct) <> do
371 W.fill (max 0 (max_amount_length - amounts_length amounts)) W.empty <> do
372 -- NOTE: AFAICS Text.PrettyPrint.Leijen gives no way
373 -- to get the column size of a Doc
374 -- before printing it, hence the call to amounts_length here again.
376 (W.space <> (W.bold $ W.yellow $ W.char Read.amount_sep) <> W.space)
380 [c] -> W.space <> comment c
381 _ -> W.line <> do comments (W.text "\t\t") cmts)
383 status :: Transaction.Status -> Doc
388 -- ** Mesuring 'Posting'
390 type Posting_Lengths = (Int, Int)
392 nil_Posting_Lengths :: Posting_Lengths
393 nil_Posting_Lengths = (0, 0)
395 postings_lengths :: Posting.Type -> Posting.By_Account -> Posting_Lengths -> Posting_Lengths
396 postings_lengths type_ =
397 flip $ Data.Map.foldl $ Data.List.foldl $
399 (max (account_length type_ (Posting.account p)))
401 (max (amounts_length (Posting.amounts p)))
403 -- * Printing 'Transaction'
405 transaction :: Transaction -> Doc
406 transaction t = transaction_with_lengths (transaction_lengths t nil_Transaction_Lengths) t
408 transaction_with_lengths :: Transaction_Lengths -> Transaction -> Doc
409 transaction_with_lengths
411 Transaction.Transaction
412 { Transaction.code=code_
413 , Transaction.comments_before
414 , Transaction.comments_after
415 , Transaction.dates=(first_date, dates)
416 , Transaction.description
417 , Transaction.postings
418 , Transaction.virtual_postings
419 , Transaction.balanced_virtual_postings
420 , Transaction.status=status_
421 -- , Transaction.tags
423 (case comments_before of
425 _ -> comments (W.text "\t") comments_before <> W.line) <> do
427 Data.List.intersperse
428 (W.char Read.date_sep)
429 (Data.List.map date (first_date:dates))) <> do
431 True -> W.space <> status status_
432 False -> W.empty) <> do
436 _ -> W.space <> (W.dullmagenta $ text description)) <> do
438 (case comments_after of
440 _ -> comments (W.text "\t") comments_after <> W.line) <> do
441 W.vsep $ Data.List.map
445 (W.vsep . Data.List.map
446 (posting posting_lengths_ type_)))
447 (Posting.by_signs_and_account ps))
448 [ (Posting.Type_Regular, postings)
449 , (Posting.Type_Virtual, virtual_postings)
450 , (Posting.Type_Virtual_Balanced, balanced_virtual_postings)
453 code :: Transaction.Code -> Doc
456 t -> W.space <> W.char '(' <> text t <> W.char ')'
458 -- ** Mesuring 'Transaction'
460 type Transaction_Lengths = Posting_Lengths
462 nil_Transaction_Lengths :: Posting_Lengths
463 nil_Transaction_Lengths = nil_Posting_Lengths
465 transaction_lengths :: Transaction -> Posting_Lengths -> Posting_Lengths
467 Transaction.Transaction
468 { Transaction.postings
469 , Transaction.virtual_postings
470 , Transaction.balanced_virtual_postings
471 } posting_lengths_ = do
473 (flip (\(type_, ps) -> postings_lengths type_ ps))
475 [ (Posting.Type_Regular, postings)
476 , (Posting.Type_Virtual, virtual_postings)
477 , (Posting.Type_Virtual_Balanced, balanced_virtual_postings)
480 -- * Printing 'Journal'
482 journal :: Journal -> Doc
483 journal Journal.Journal
484 { Journal.transactions
486 let transaction_lengths_ =
488 (Data.List.foldl (flip transaction_lengths))
489 nil_Transaction_Lengths
492 (Data.List.foldl (\doc t ->
493 (if W.is_empty doc then W.empty else doc <> W.line)
494 <> transaction_with_lengths transaction_lengths_ t <> W.line