1 {-# LANGUAGE DataKinds #-}
2 {-# LANGUAGE FlexibleContexts #-}
3 {-# LANGUAGE FlexibleInstances #-}
4 {-# LANGUAGE MagicHash #-}
5 {-# LANGUAGE MultiParamTypeClasses #-}
6 {-# LANGUAGE NamedFieldPuns #-}
7 {-# LANGUAGE OverloadedStrings #-}
8 {-# LANGUAGE ScopedTypeVariables #-}
9 {-# LANGUAGE TypeFamilies #-}
10 module Hcompta.LCC.Write where
12 -- import Control.Monad (Monad)
13 -- import Data.Time.LocalTime (TimeZone(..))
14 -- import qualified Control.Monad.Classes as MC
15 -- import qualified Control.Monad.Trans.Reader as R
16 -- import qualified Data.Time.Calendar as Time
17 -- import qualified Data.Time.LocalTime as Time
19 import Data.Char (Char)
21 import Data.Eq (Eq(..))
22 import Data.Foldable (Foldable(..))
23 import Data.Function (($), (.), flip, id)
24 import Control.Monad (Monad(..))
25 import Data.Functor ((<$>))
26 import Data.Functor.Compose (Compose(..))
27 import Data.Maybe (Maybe(..))
28 import Data.Monoid ((<>))
29 import Data.Ord (Ord(..))
30 import Data.Tuple (fst)
31 import GHC.Exts (Int(..))
32 import GHC.Integer.Logarithms (integerLogBase#)
33 import Prelude (Bounded(..), Integer, Num(..), RealFrac(..), Show(..), Integral(..), fromIntegral)
34 import System.IO (IO, Handle)
35 import Text.WalderLeijen.ANSI.Text (Doc)
36 import qualified Data.Char as Char
37 import qualified Data.List as List
38 import qualified Data.Map.Strict as Map
39 import qualified Data.MonoTraversable as MT
40 import qualified Data.NonNull as NonNull
41 import qualified Data.Strict as S
42 import qualified Data.Text as Text
43 import qualified Data.Text.Lazy as TL
44 import qualified Data.TreeMap.Strict as TreeMap
45 import qualified Text.WalderLeijen.ANSI.Text as W
46 import qualified Data.ByteString as BS
47 import qualified Data.Text.Encoding as Enc
49 import qualified Hcompta as H
51 import Hcompta.LCC.Account
52 import Hcompta.LCC.Amount
53 import Hcompta.LCC.Chart
54 import Hcompta.LCC.Journal
55 import Hcompta.LCC.Name
56 import Hcompta.LCC.Posting
57 import Hcompta.LCC.Tag
58 import Hcompta.LCC.Transaction
59 import Hcompta.LCC.Grammar
60 import Hcompta.LCC.Compta
61 -- import qualified Hcompta.LCC.Lib.Strict as S
64 write_date :: Date -> Doc
66 let (y, mo, d) = H.date_gregorian dat in
67 (if y == 0 then W.empty else W.integer y <> sep char_ymd_sep) <>
69 sep char_ymd_sep <> doc_int2 d <>
70 (case H.date_tod dat of
73 sep '_' <> doc_int2 h <>
74 sep ':' <> doc_int2 m <>
78 (if s < 10 then W.char '0' else W.empty) <>
79 W.strict_text (Text.pack $ show $ (truncate s::Integer)))) {-<>
82 _ | tz_name /= "" -> W.space <> do W.strict_text $ Text.pack tz_name
83 _ -> W.space <> do W.strict_text $ Text.pack $ Time.timeZoneOffsetString tz)
86 doc_int2 :: Int -> Doc
87 doc_int2 i = if i < 10 then W.char '0' <> W.int i else W.int i
91 width_date :: Date -> Int
93 let (y, _, _) = H.date_gregorian dat
97 (if y < 0 then 1 else 0) -- sign
98 + (1 + (I# (integerLogBase# 10 (abs y)))) -- year
103 + (case H.date_tod dat of
117 write_account :: Account -> Doc
120 gray (W.char char_account_sep) <>
121 write_account_section a
123 write_account_section :: Account_Section -> Doc
124 write_account_section = W.strict_text . unName
126 width_account :: Account -> Int
129 (\acc -> (1 +) . (acc +) . Text.length . unName)
132 -- ** Write 'Account_Ref'
133 write_account_ref :: Tag_Path -> Doc
134 write_account_ref (Tag_Path path) =
136 (:) (op $ W.char char_account_tag_prefix) $
138 (op $ W.char char_tag_sep)
139 (W.strict_text . unName <$> NonNull.toNullable path)
140 where op = W.bold . W.dullyellow
142 width_account_ref :: Tag_Path -> Int
143 width_account_ref (Tag_Path anch) =
145 (\acc -> (1 +) . (acc +) . MT.olength)
148 -- ** Write 'Account_Tag'
149 write_account_tag :: Account_Tag -> Doc
150 write_account_tag (Account_Tag (Tag (Tag_Path path) (Tag_Data value))) =
152 (:) (op $ W.char char_account_tag_prefix) $
154 (op $ W.char char_tag_sep)
155 (W.strict_text . unName <$> NonNull.toNullable path) ) <>
159 op (W.char char_tag_data_prefix) <>
161 where op = W.bold . W.dullyellow
164 write_amount :: Styled_Amount Amount -> Doc
167 { style_amount_unit_side
168 , style_amount_unit_spaced
171 let unt = amount_unit amt in
172 case style_amount_unit_side of
175 case style_amount_unit_spaced of
176 S.Just True | unt /= H.unit_empty -> W.space
179 <> write_quantity (sty, amount_quantity amt)
180 <> case style_amount_unit_side of
182 (case style_amount_unit_spaced of
183 S.Just True | unt /= H.unit_empty -> W.space
187 (case style_amount_unit_spaced of
188 S.Just True | unt /= H.unit_empty -> W.space
193 width_amount :: Styled_Amount Amount -> Int
194 width_amount (sty@(Style_Amount { style_amount_unit_spaced }), amt) =
195 let unit = amount_unit amt in
197 + (case style_amount_unit_spaced of
198 S.Just True | unit /= H.unit_empty -> 1
200 + width_quantity sty (amount_quantity amt)
203 write_unit :: Unit -> Doc
205 let t = H.unit_text u in
208 (\c -> case Char.generalCategory c of
209 Char.CurrencySymbol -> True
210 Char.LowercaseLetter -> True
211 Char.ModifierLetter -> True
212 Char.OtherLetter -> True
213 Char.TitlecaseLetter -> True
214 Char.UppercaseLetter -> True
218 else W.hcat $ W.strict_text <$> ["\"", t, "\""]
220 width_unit :: Unit -> Int
222 let t = H.unit_text u in
225 (\c -> case Char.generalCategory c of
226 Char.CurrencySymbol -> True
227 Char.LowercaseLetter -> True
228 Char.ModifierLetter -> True
229 Char.OtherLetter -> True
230 Char.TitlecaseLetter -> True
231 Char.UppercaseLetter -> True
236 -- * Write 'Quantity'
237 write_quantity :: Styled_Amount Quantity -> Doc
240 { style_amount_fractioning
241 , style_amount_grouping_integral
242 , style_amount_grouping_fractional
245 let Decimal e n = qty
246 let num = show $ abs $ n
247 let sign = W.bold $ W.yellow $ W.strict_text (if n < 0 then "-" else "")
249 then sign <> W.bold (W.blue $ W.strict_text $ Text.pack num)
251 let num_len = List.length num
254 [ List.replicate (fromIntegral e + 1 - num_len) '0'
256 -- , replicate (fromIntegral precision - fromIntegral e) '0'
258 let (int, frac) = List.splitAt (max 1 (num_len - fromIntegral e)) padded
259 let default_fractioning =
261 del_grouping_sep style_amount_grouping_integral $
262 del_grouping_sep style_amount_grouping_fractional $
266 W.text (TL.pack $ S.maybe id
267 (\g -> List.reverse . group g . List.reverse)
268 style_amount_grouping_integral $ int) <>
269 W.yellow (W.char (S.fromMaybe default_fractioning style_amount_fractioning)) <>
270 W.text (TL.pack $ S.maybe id group style_amount_grouping_fractional frac))
272 group :: Style_Amount_Grouping -> [Char] -> [Char]
273 group (Style_Amount_Grouping sep sizes_) =
274 List.concat . List.reverse .
275 List.map List.reverse . fst .
277 (flip (\digit x -> case x of
278 ([], sizes) -> ([[digit]], sizes)
279 (digits:groups, []) -> ((digit:digits):groups, [])
280 (digits:groups, curr_sizes@(size:sizes)) ->
281 if List.length digits < size
282 then ( (digit:digits):groups, curr_sizes)
283 else ([digit]:[sep]:digits:groups, if List.null sizes then curr_sizes else sizes)
286 del_grouping_sep grouping =
288 S.Just (Style_Amount_Grouping sep _) -> List.delete sep
291 width_quantity :: Style_Amount -> Quantity -> Int
292 width_quantity Style_Amount
293 { style_amount_grouping_integral
294 , style_amount_grouping_fractional
296 let Decimal e n = qty in
297 let sign_len = if n < 0 then 1 else 0 in
298 let fractioning_len = if e > 0 then 1 else 0 in
299 let num_len = if n == 0 then 0 else (1 +) $ I# (integerLogBase# 10 (abs n)) in
300 let pad_left_len = max 0 (fromIntegral e + 1 - num_len) in
301 -- let pad_right_len = max 0 (fromIntegral precision - fromIntegral e) in
302 let padded_len = pad_left_len + num_len {-+ pad_right_len-} in
303 let int_len = max 1 (num_len - fromIntegral e) in
304 let frac_len = max 0 (padded_len - int_len) in
308 + S.maybe 0 (group int_len) style_amount_grouping_integral
309 + S.maybe 0 (group frac_len) style_amount_grouping_fractional
312 group :: Int -> Style_Amount_Grouping -> Int
313 group num_len (Style_Amount_Grouping _sep sizes_) =
316 else loop 0 num_len sizes_
318 loop :: Int -> Int -> [Int] -> Int
323 let l = len - size in
325 else loop (pad + 1) l sizes
327 let l = len - size in
329 else loop (pad + 1) l sizes
332 write_comment :: Comment -> Doc
333 write_comment (Comment com) =
335 W.char char_comment_prefix
336 <> (case Text.uncons com of
337 Just (c, _) | not $ Char.isSpace c -> W.space
341 write_comments :: Doc -> [Comment] -> Doc
342 write_comments prefix =
344 List.intersperse W.line .
345 List.map (\c -> prefix <> write_comment c)
354 , posting_account_ref
356 , posting_comments=cmts
361 let (doc_acct, wi_acct) =
362 case posting_account_ref of
363 S.Just (a S.:!: sa) | context_write_account_ref ctx ->
364 ( write_account_ref a <> S.maybe W.empty write_account sa
365 , width_account_ref a + S.maybe 0 width_account sa )
367 ( write_account posting_account
368 , width_account posting_account ) in
369 (case posting_amounts of
370 Amounts amts | Map.null amts -> doc_acct
374 let amt = styled_amount (context_write_amounts ctx) $ Amount unit qty in
375 let wi_amt = width_amount amt in
377 (if W.is_empty doc then W.empty else W.line <> W.string " ") <>
379 W.fill (context_write_max_posting_width ctx - (wi_acct + wi_amt)) W.space <>
384 [c] -> W.space <> write_comment c
385 _ -> W.line <> write_comments (W.text " ") cmts)
387 -- ** Type 'Widths_Posting'
388 type Widths_Posting = Int
394 widths_postings ctx (Postings ps) =
396 ((case posting_account_ref p of
397 S.Just (a S.:!: sa) | context_write_account_ref ctx ->
398 width_account_ref a +
399 S.maybe 0 width_account sa
400 _ -> width_account (posting_account p)
402 (\len -> if len > 0 then 1 + len else len) $
406 styled_amount (context_write_amounts ctx) $
408 0 (unAmounts $ posting_amounts p)
412 -- * Write 'Transaction'
415 -> Transaction -> Doc
416 write_transaction ctx
418 { transaction_comments
420 , transaction_wording = Wording transaction_wording
421 , transaction_postings = Postings transaction_postings
422 , transaction_tags = Transaction_Tags (Tags tags)
424 let ctx' = ctx { context_write_max_posting_width =
425 let wi = context_write_max_posting_width ctx in
427 then widths_transaction ctx t
431 (W.char char_transaction_date_sep)
432 (write_date <$> NonNull.toNullable transaction_dates)) <>
433 (case transaction_wording of
435 _ -> W.space <> W.dullmagenta (W.strict_text transaction_wording)) <>
437 (case transaction_comments of
439 _ -> write_comments W.space transaction_comments <> W.line) <>
440 TreeMap.foldr_with_Path
442 foldr (\value -> (<>) (W.string " " <>
443 write_transaction_tag (Transaction_Tag (Tag (Tag_Path path) value)) <> W.line)))
446 (W.vsep . (write_posting ctx' <$>))
452 -> j Transaction -> Doc
453 write_transactions ctx j =
454 let ctx' = ctx{context_write_max_posting_width =
455 foldr (max . widths_transaction ctx) 0 j} in
457 write_transaction ctx' t <>
458 (if W.is_empty doc then W.line else W.line <> W.line <> doc)
461 -- ** Type 'Widths_Transaction'
462 type Widths_Transaction = Widths_Posting
464 widths_transaction :: Context_Write -> Transaction -> Widths_Posting
465 widths_transaction ctx
467 { transaction_postings
470 (max . widths_postings ctx)
471 0 [ transaction_postings ]
473 -- ** Write 'Transaction_Tag'
474 write_transaction_tag :: Transaction_Tag -> Doc
475 write_transaction_tag (Transaction_Tag (Tag (Tag_Path path) (Tag_Data value))) =
477 (:) (W.bold $ W.dullyellow $ W.char char_tag_prefix) $
479 (op $ W.char char_tag_sep)
480 (write_transaction_tag_section <$> NonNull.toNullable path)) <>
484 op (W.char char_tag_data_prefix) <>
487 op = W.bold . W.yellow
489 write_transaction_tag_section :: Name -> Doc
490 write_transaction_tag_section = W.bold . W.strict_text . unName
493 write_journal :: Foldable j => Context_Write -> Journal (j [Transaction]) -> Doc
494 write_journal ctx jnl =
495 write_transactions ctx $
496 Compose $ journal_content jnl
498 -- * Write 'Journals'
499 write_journals :: Foldable j => Context_Write -> Journals (j [Transaction]) -> Doc
500 write_journals ctx (Journals js) =
502 (\doc j@Journal{journal_file=PathFile jf, journal_content=jc} ->
504 write_comment (Comment $ Text.pack jf) <> W.line <>
505 if null jc then W.empty else (W.line <> write_journal ctx j)
509 write_chart :: Chart -> Doc
511 TreeMap.foldl_with_Path
512 (\doc acct (Account_Tags (Tags ca)) ->
514 write_account (H.get acct) <> W.line <>
515 TreeMap.foldl_with_Path
520 ddd <> W.string " " <>
521 write_account_tag (Account_Tag (Tag (Tag_Path tp) tv)) <>
532 write_terms :: Terms -> Doc
542 write_compta :: Context_Write -> Compta src ss -> Doc
543 write_compta ctx Compta
545 , compta_chart=c@Chart{chart_accounts=ca}
546 , compta_style_amounts=amts
549 (if null ts then W.empty else (write_terms ts <> W.line)) <>
550 (if TreeMap.null ca then W.empty else (write_chart c <> W.line)) <>
551 write_journals ctx{context_write_amounts=context_write_amounts ctx <> amts} js
553 -- * Write 'SourcePos'
554 write_sourcepos :: SourcePos -> IO Doc
555 write_sourcepos (SourcePos p (PosFile l) (PosFile c)) = do
556 content <- Enc.decodeUtf8 <$> BS.readFile p
557 let ls = Text.lines content
558 let ll = max 1 $ l - size_ctx
560 List.take (intFrom $ (l - ll) + 1 + size_ctx) $
561 List.drop (intFrom $ ll-1) ls
562 let ns = show <$> List.take (List.length qs) [ll..]
563 let max_len_n = maximum $ List.length <$> ns
564 let ns' = (<$> ns) $ \n ->
565 List.replicate (max_len_n - List.length n) ' ' <> n
568 List.zipWith (\(n, sn) q ->
569 " " <> gray (W.strict_text (Text.pack sn)) <>
570 " " <> (if n == l then mark q else W.strict_text q)
571 ) (List.zip [ll..] ns') qs
572 return $ quote <> W.line
575 intFrom = fromInteger . toInteger
577 let (b, a) = Text.splitAt (intFrom c - 1) q in
579 case Text.uncons a of
581 Just (a0, a') -> red (W.char a0) <> W.strict_text a'
584 gray = W.bold . W.dullblack
590 -- Type 'Context_Write'
593 { context_write_account_ref :: Bool
594 , context_write_amounts :: Style_Amounts
595 , context_write_max_posting_width :: Int
598 context_write :: Context_Write
601 { context_write_account_ref = True
602 , context_write_amounts = Style_Amounts Map.empty
603 , context_write_max_posting_width = 0
607 type Style_Anchor = Bool
608 type instance MC.CanDo (S.ReaderT Context_Write m) (MC.EffReader Context_Write) = 'True
609 instance Monad m => MC.MonadReaderN 'MC.Zero Context_Write (S.ReaderT Context_Write m) where
610 askN _px = S.ReaderT R.ask
611 type instance MC.CanDo (S.ReaderT Context_Write m) (MC.EffReader Style_Anchor) = 'True
612 instance Monad m => MC.MonadReaderN 'MC.Zero Style_Anchor (S.ReaderT Context_Write m) where
613 askN _px = S.ReaderT $ R.asks $ Style_Anchor . context_write_account_ref
616 -- * Type 'Style_Write'
619 { style_write_align :: Bool
620 , style_write_color :: Bool
622 style_write :: Style_Write
625 { style_write_align = True
626 , style_write_color = True
630 write :: Style_Write -> Doc -> TL.Text
633 , style_write_align } =
636 then W.renderPretty style_write_color 1.0 maxBound
637 else W.renderCompact style_write_color
639 writeIO :: Style_Write -> Handle -> Doc -> IO ()
646 then W.renderPretty style_write_color 1.0 maxBound doc
647 else W.renderCompact style_write_color doc