1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE MagicHash #-}
4 {-# LANGUAGE NamedFieldPuns #-}
5 {-# LANGUAGE OverloadedStrings #-}
6 {-# LANGUAGE ScopedTypeVariables #-}
7 {-# LANGUAGE TypeFamilies #-}
8 module Hcompta.LCC.Write where
11 import Data.Char (Char)
12 import qualified Data.Char as Char
14 import Data.Eq (Eq(..))
15 import Data.Foldable (Foldable)
16 import qualified Data.Foldable as Foldable
17 import Data.Function (($), (.), flip, id)
18 import Data.Functor ((<$>))
19 import qualified Data.Functor.Compose
20 import qualified Data.List as List
21 import qualified Data.List.NonEmpty as NonEmpty
22 import qualified Data.Map.Strict as Map
23 import Data.Maybe (Maybe(..), maybe, fromMaybe)
24 import Data.Monoid ((<>))
25 import qualified Data.MonoTraversable as MT
26 import qualified Data.NonNull as NonNull
27 import Data.Ord (Ord(..))
28 import qualified Data.Text as Text
29 import qualified Data.Text.Lazy as TL
30 import qualified Data.TreeMap.Strict as TreeMap
31 import Data.Tuple (fst)
32 import GHC.Exts (Int(..))
33 import GHC.Integer.Logarithms (integerLogBase#)
34 import Prelude (Bounded(..), Integer, Num(..), RealFrac(..), Show(..), fromIntegral)
35 import System.IO (IO, Handle)
36 import Text.WalderLeijen.ANSI.Text (Doc)
37 import qualified Text.WalderLeijen.ANSI.Text as W
39 import qualified Hcompta as H
41 import Hcompta.LCC.Account
42 import Hcompta.LCC.Amount
43 import Hcompta.LCC.Anchor
44 import Hcompta.LCC.Chart
45 import Hcompta.LCC.Journal
46 import Hcompta.LCC.Name
47 import Hcompta.LCC.Posting
48 import Hcompta.LCC.Read
49 import Hcompta.LCC.Tag
50 import Hcompta.LCC.Transaction
53 write_date :: Date -> Doc
55 let (y, mo, d) = H.date_gregorian dat in
56 (if y == 0 then W.empty else W.integer y <> sep char_date_ymd_sep) <>
58 sep char_date_ymd_sep <> int2 d <>
59 (case H.date_tod dat of
67 (if s < 10 then W.char '0' else W.empty) <>
68 W.strict_text (Text.pack $ show $ (truncate s::Integer))))
71 -- _ | tz_name /= "" -> W.space <> do W.strict_text $ Text.pack tz_name
72 -- _ -> W.space <> do W.strict_text $ Text.pack $ Time.timeZoneOffsetString tz)
75 int2 i = if i < 10 then W.char '0' <> W.int i else W.int i
77 sep = W.bold . W.dullblack . W.char
79 write_date_length :: Date -> Int
80 write_date_length dat = do
81 let (y, _, _) = H.date_gregorian dat
85 (if y < 0 then 1 else 0) -- sign
86 + (1 + (I# (integerLogBase# 10 (abs y)))) -- year
91 + (case H.date_tod dat of
105 write_account :: Account -> Doc
108 W.bold (W.dullblack $ W.char char_account_sep) <>
109 write_account_section a
111 write_account_section :: Account_Section -> Doc
112 write_account_section = W.strict_text . unName
114 write_account_length :: Account -> Int
115 write_account_length =
117 (\acc -> (1 +) . (acc +) . Text.length . unName)
120 -- ** Write 'Account_Anchor'
121 write_account_anchor :: Account_Anchor -> Doc
122 write_account_anchor (Account_Anchor (Anchor anchor)) =
124 (:) (op $ W.char char_account_anchor_prefix) $
126 (op $ W.char char_account_anchor_sep)
127 (W.strict_text . unName <$> NonNull.toNullable anchor)
128 where op = W.bold . W.dullyellow
130 write_account_anchor_length :: Account_Anchor -> Int
131 write_account_anchor_length (Account_Anchor anch) =
133 (\acc -> (1 +) . (acc +) . MT.olength)
136 -- ** Write 'Account_Tag'
137 write_account_tag :: Account_Tag -> Doc
138 write_account_tag (Account_Tag (Tag (Tag_Path path) (Tag_Value value))) =
140 (:) (op $ W.char char_account_tag_prefix) $
142 (op $ W.char char_tag_section_sep)
143 (W.strict_text . unName <$> NonEmpty.toList path) ) <>
147 op (W.char char_tag_value_prefix) <>
149 where op = W.bold . W.dullyellow
152 write_amount :: Amount_Styled Amount -> Doc
155 { amount_style_unit_side
156 , amount_style_unit_spaced
159 let unt = amount_unit amt in
160 case amount_style_unit_side of
161 Just Amount_Style_Side_Left ->
163 case amount_style_unit_spaced of
164 Just True | unt /= H.unit_empty -> W.space
167 <> write_quantity (sty, amount_quantity amt)
168 <> case amount_style_unit_side of
169 (Just Amount_Style_Side_Right) ->
170 (case amount_style_unit_spaced of
171 Just True | unt /= H.unit_empty -> W.space
175 (case amount_style_unit_spaced of
176 Just True | unt /= H.unit_empty -> W.space
181 write_amount_length :: Amount_Styled Amount -> Int
182 write_amount_length (sty@(Amount_Style { amount_style_unit_spaced }), amt) =
183 let unit = amount_unit amt in
184 write_unit_length unit
185 + (case amount_style_unit_spaced of
186 Just True | unit /= H.unit_empty -> 1
188 + write_quantity_length sty (amount_quantity amt)
191 write_unit :: Unit -> Doc
193 let t = H.unit_text u in
196 (\c -> case Char.generalCategory c of
197 Char.CurrencySymbol -> True
198 Char.LowercaseLetter -> True
199 Char.ModifierLetter -> True
200 Char.OtherLetter -> True
201 Char.TitlecaseLetter -> True
202 Char.UppercaseLetter -> True
206 else W.hcat $ W.strict_text <$> ["\"", t, "\""]
208 write_unit_length :: Unit -> Int
209 write_unit_length u =
210 let t = H.unit_text u in
213 (\c -> case Char.generalCategory c of
214 Char.CurrencySymbol -> True
215 Char.LowercaseLetter -> True
216 Char.ModifierLetter -> True
217 Char.OtherLetter -> True
218 Char.TitlecaseLetter -> True
219 Char.UppercaseLetter -> True
224 -- * Write 'Quantity'
225 write_quantity :: Amount_Styled Quantity -> Doc
228 { amount_style_fractioning
229 , amount_style_grouping_integral
230 , amount_style_grouping_fractional
233 let Decimal e n = qty
234 let num = show $ abs $ n
235 let sign = W.bold $ W.yellow $ W.strict_text (if n < 0 then "-" else "")
237 then sign <> W.bold (W.blue $ W.strict_text $ Text.pack num)
239 let num_len = List.length num
242 [ List.replicate (fromIntegral e + 1 - num_len) '0'
244 -- , replicate (fromIntegral precision - fromIntegral e) '0'
246 let (int, frac) = List.splitAt (max 1 (num_len - fromIntegral e)) padded
247 let default_fractioning =
249 del_grouping_sep amount_style_grouping_integral $
250 del_grouping_sep amount_style_grouping_fractional $
254 W.text (TL.pack $ maybe id
255 (\g -> List.reverse . group g . List.reverse)
256 amount_style_grouping_integral $ int) <>
257 W.yellow (W.char (fromMaybe default_fractioning amount_style_fractioning)) <>
258 W.text (TL.pack $ maybe id group amount_style_grouping_fractional frac))
260 group :: Amount_Style_Grouping -> [Char] -> [Char]
261 group (Amount_Style_Grouping sep sizes_) =
262 List.concat . List.reverse .
263 List.map List.reverse . fst .
265 (flip (\digit x -> case x of
266 ([], sizes) -> ([[digit]], sizes)
267 (digits:groups, []) -> ((digit:digits):groups, [])
268 (digits:groups, curr_sizes@(size:sizes)) ->
269 if List.length digits < size
270 then ( (digit:digits):groups, curr_sizes)
271 else ([digit]:[sep]:digits:groups, if List.null sizes then curr_sizes else sizes)
274 del_grouping_sep grouping =
276 Just (Amount_Style_Grouping sep _) -> List.delete sep
279 write_quantity_length :: Amount_Style -> Quantity -> Int
280 write_quantity_length Amount_Style
281 { amount_style_grouping_integral
282 , amount_style_grouping_fractional
284 let Decimal e n = qty in
285 let sign_len = if n < 0 then 1 else 0 in
286 let fractioning_len = if e > 0 then 1 else 0 in
287 let num_len = if n == 0 then 0 else (1 +) $ I# (integerLogBase# 10 (abs n)) in
288 let pad_left_len = max 0 (fromIntegral e + 1 - num_len) in
289 -- let pad_right_len = max 0 (fromIntegral precision - fromIntegral e) in
290 let padded_len = pad_left_len + num_len {-+ pad_right_len-} in
291 let int_len = max 1 (num_len - fromIntegral e) in
292 let frac_len = max 0 (padded_len - int_len) in
296 + maybe 0 (group int_len) amount_style_grouping_integral
297 + maybe 0 (group frac_len) amount_style_grouping_fractional
300 group :: Int -> Amount_Style_Grouping -> Int
301 group num_len (Amount_Style_Grouping _sep sizes_) =
304 else loop 0 num_len sizes_
306 loop :: Int -> Int -> [Int] -> Int
311 let l = len - size in
313 else loop (pad + 1) l sizes
315 let l = len - size in
317 else loop (pad + 1) l sizes
320 write_comment :: Comment -> Doc
321 write_comment (Comment com) =
323 W.char char_comment_prefix
324 <> (case Text.uncons com of
325 Just (c, _) | not $ Char.isSpace c -> W.space
329 write_comments :: Doc -> [Comment] -> Doc
330 write_comments prefix =
332 List.intersperse W.line .
333 List.map (\c -> prefix <> write_comment c)
336 write_posting :: Amount_Styles -> Posting_Lengths -> Posting -> Doc
337 write_posting styles max_posting_length
340 , posting_account_anchor
342 , posting_comments=cmts
347 let (doc_acct, len_acct) =
348 case posting_account_anchor of
350 ( write_account posting_account
351 , write_account_length posting_account )
353 ( write_account_anchor a <> maybe W.empty write_account sa
354 , write_account_anchor_length a + maybe 0 write_account_length sa ) in
355 (case posting_amounts of
356 Amounts amts | Map.null amts -> doc_acct
360 let amt = amount_styled styles $ Amount unit qty in
361 let len_amt = write_amount_length amt in
363 (if W.is_empty doc then W.empty else W.line <> W.string " ") <>
365 W.fill (max_posting_length - (len_acct + len_amt)) W.space <>
370 [c] -> W.space <> write_comment c
371 _ -> W.line <> write_comments (W.text " ") cmts)
373 -- ** Type 'Posting_Lengths'
374 type Posting_Lengths = Int
376 write_postings_lengths
381 write_postings_lengths styles (Postings ps) pl =
382 Foldable.foldr (\Posting{posting_account=acct, posting_amounts=Amounts amts} -> max $
383 (write_account_length acct +) $
384 (\len -> if len > 0 then 1 + len else len) $
387 write_amount_length $
388 amount_styled styles $
392 (Data.Functor.Compose.Compose ps)
394 -- * Write 'Transaction'
395 write_transaction :: Amount_Styles -> Transaction -> Doc
396 write_transaction styles t =
397 write_transaction_with_lengths
398 styles (write_transaction_lengths styles t 0) t
400 write_transactions :: Foldable j => Amount_Styles -> j Transaction -> Doc
401 write_transactions styles j = do
402 let transaction_lengths_ =
403 Foldable.foldr (write_transaction_lengths styles) 0 j
404 Foldable.foldr (\t doc ->
405 write_transaction_with_lengths styles transaction_lengths_ t <>
406 (if W.is_empty doc then W.empty else W.line <> doc)
409 write_transaction_with_lengths
411 -> Transaction_Lengths
412 -> Transaction -> Doc
413 write_transaction_with_lengths
417 { transaction_comments
419 , transaction_wording=Wording transaction_wording
420 , transaction_postings=Postings transaction_postings
421 , transaction_anchors=Transaction_Anchors (Anchors anchors)
422 , transaction_tags=Transaction_Tags (Tags tags)
426 (W.char char_transaction_date_sep)
427 (write_date <$> NonNull.toNullable transaction_dates)) <>
428 (case transaction_wording of
430 _ -> W.space <> W.dullmagenta (W.strict_text transaction_wording)) <>
432 (case transaction_comments of
434 _ -> write_comments W.space transaction_comments <> W.line) <>
436 (\path () -> ((W.string " " <>
437 write_transaction_anchor (Transaction_Anchor path) <> W.line) <>))
439 TreeMap.foldr_with_Path
441 Foldable.foldr (\value -> (<>) (W.string " " <>
442 write_transaction_tag (Transaction_Tag (Tag (Tag_Path path) value)) <> W.line)))
445 (W.vsep . (write_posting styles posting_lengths_ <$>))
446 transaction_postings <> W.line
448 -- ** Type 'Transaction_Lengths'
449 type Transaction_Lengths = Posting_Lengths
451 write_transaction_lengths
456 write_transaction_lengths
459 { transaction_postings
462 (flip $ write_postings_lengths styles)
464 [ transaction_postings ]
466 -- ** Write 'Transaction_Tag'
467 write_transaction_tag :: Transaction_Tag -> Doc
468 write_transaction_tag (Transaction_Tag (Tag (Tag_Path path) (Tag_Value value))) =
470 (:) (W.bold $ W.dullyellow $ W.char char_transaction_tag_prefix) $
472 (op $ W.char char_tag_section_sep)
473 (write_transaction_tag_section <$> NonEmpty.toList path)) <>
477 op (W.char char_tag_value_prefix) <>
480 op = W.bold . W.yellow
482 write_transaction_tag_section :: Name -> Doc
483 write_transaction_tag_section = W.bold . W.strict_text . unName
485 -- ** Write 'Transaction_Anchor'
486 write_transaction_anchor :: Transaction_Anchor -> Doc
487 write_transaction_anchor (Transaction_Anchor (Anchor anch)) =
489 (:) (op $ W.char char_transaction_anchor_prefix) $
491 (op $ W.char char_anchor_section_sep)
492 (write_transaction_anchor_section <$> NonNull.toNullable anch)
494 op = W.bold . W.yellow
496 write_transaction_anchor_section :: Name -> Doc
497 write_transaction_anchor_section = W.bold . W.strict_text . unName
500 write_journal :: Foldable j => Journal (j Transaction) -> Doc
501 write_journal Journal
502 { journal_amount_styles
504 } = write_transactions journal_amount_styles journal_content
507 write_chart :: Chart -> Doc
509 TreeMap.foldl_with_Path
510 (\doc acct (Account_Tags (Tags ca)) ->
512 write_account (H.get acct) <> W.line <>
513 TreeMap.foldl_with_Path
518 ddd <> W.string " " <>
519 write_account_tag (Account_Tag (Tag (Tag_Path tp) tv)) <>
528 -- * Type 'Write_Style'
531 { write_style_align :: Bool
532 , write_style_color :: Bool
534 write_style :: Write_Style
537 { write_style_align = True
538 , write_style_color = True
542 write :: Write_Style -> Doc -> TL.Text
545 , write_style_align } =
548 then W.renderPretty write_style_color 1.0 maxBound
549 else W.renderCompact write_style_color
551 writeIO :: Write_Style -> Doc -> Handle -> IO ()
558 then W.renderPretty write_style_color 1.0 maxBound doc
559 else W.renderCompact write_style_color doc