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 Data.Functor ((<$>))
25 import Data.Functor.Compose (Compose(..))
26 import Data.Maybe (Maybe(..))
27 import Data.Monoid ((<>))
28 import Data.Ord (Ord(..))
29 import Data.Tuple (fst)
30 import GHC.Exts (Int(..))
31 import GHC.Integer.Logarithms (integerLogBase#)
32 import Prelude (Bounded(..), Integer, Num(..), RealFrac(..), Show(..), fromIntegral)
33 import System.IO (IO, Handle)
34 import Text.WalderLeijen.ANSI.Text (Doc)
35 import qualified Data.Char as Char
36 import qualified Data.List as List
37 import qualified Data.Map.Strict as Map
38 import qualified Data.MonoTraversable as MT
39 import qualified Data.NonNull as NonNull
40 import qualified Data.Strict as S
41 import qualified Data.Text as Text
42 import qualified Data.Text.Lazy as TL
43 import qualified Data.TreeMap.Strict as TreeMap
44 import qualified Text.WalderLeijen.ANSI.Text as W
46 import qualified Hcompta as H
48 import Hcompta.LCC.Account
49 import Hcompta.LCC.Amount
50 import Hcompta.LCC.Chart
51 import Hcompta.LCC.Journal
52 import Hcompta.LCC.Name
53 import Hcompta.LCC.Posting
54 import Hcompta.LCC.Read
55 import Hcompta.LCC.Tag
56 import Hcompta.LCC.Transaction
57 -- import qualified Hcompta.LCC.Lib.Strict as S
60 write_date :: Date -> Doc
62 let (y, mo, d) = H.date_gregorian dat in
63 (if y == 0 then W.empty else W.integer y <> sep char_ymd_sep) <>
65 sep char_ymd_sep <> doc_int2 d <>
66 (case H.date_tod dat of
69 sep '_' <> doc_int2 h <>
70 sep ':' <> doc_int2 m <>
74 (if s < 10 then W.char '0' else W.empty) <>
75 W.strict_text (Text.pack $ show $ (truncate s::Integer)))) {-<>
78 _ | tz_name /= "" -> W.space <> do W.strict_text $ Text.pack tz_name
79 _ -> W.space <> do W.strict_text $ Text.pack $ Time.timeZoneOffsetString tz)
82 doc_int2 :: Int -> Doc
83 doc_int2 i = if i < 10 then W.char '0' <> W.int i else W.int i
85 sep = W.bold . W.dullblack . W.char
87 width_date :: Date -> Int
89 let (y, _, _) = H.date_gregorian dat
93 (if y < 0 then 1 else 0) -- sign
94 + (1 + (I# (integerLogBase# 10 (abs y)))) -- year
99 + (case H.date_tod dat of
113 write_account :: Account -> Doc
116 W.bold (W.dullblack $ W.char char_account_sep) <>
117 write_account_section a
119 write_account_section :: Account_Section -> Doc
120 write_account_section = W.strict_text . unName
122 width_account :: Account -> Int
125 (\acc -> (1 +) . (acc +) . Text.length . unName)
128 -- ** Write 'Account_Ref'
129 write_account_ref :: Tag_Path -> Doc
130 write_account_ref (Tag_Path path) =
132 (:) (op $ W.char char_account_tag_prefix) $
134 (op $ W.char char_tag_sep)
135 (W.strict_text . unName <$> NonNull.toNullable path)
136 where op = W.bold . W.dullyellow
138 width_account_ref :: Tag_Path -> Int
139 width_account_ref (Tag_Path anch) =
141 (\acc -> (1 +) . (acc +) . MT.olength)
144 -- ** Write 'Account_Tag'
145 write_account_tag :: Account_Tag -> Doc
146 write_account_tag (Account_Tag (Tag (Tag_Path path) (Tag_Data value))) =
148 (:) (op $ W.char char_account_tag_prefix) $
150 (op $ W.char char_tag_sep)
151 (W.strict_text . unName <$> NonNull.toNullable path) ) <>
155 op (W.char char_tag_data_prefix) <>
157 where op = W.bold . W.dullyellow
160 write_amount :: Styled_Amount Amount -> Doc
163 { style_amount_unit_side
164 , style_amount_unit_spaced
167 let unt = amount_unit amt in
168 case style_amount_unit_side of
171 case style_amount_unit_spaced of
172 S.Just True | unt /= H.unit_empty -> W.space
175 <> write_quantity (sty, amount_quantity amt)
176 <> case style_amount_unit_side of
178 (case style_amount_unit_spaced of
179 S.Just True | unt /= H.unit_empty -> W.space
183 (case style_amount_unit_spaced of
184 S.Just True | unt /= H.unit_empty -> W.space
189 width_amount :: Styled_Amount Amount -> Int
190 width_amount (sty@(Style_Amount { style_amount_unit_spaced }), amt) =
191 let unit = amount_unit amt in
193 + (case style_amount_unit_spaced of
194 S.Just True | unit /= H.unit_empty -> 1
196 + width_quantity sty (amount_quantity amt)
199 write_unit :: Unit -> Doc
201 let t = H.unit_text u in
204 (\c -> case Char.generalCategory c of
205 Char.CurrencySymbol -> True
206 Char.LowercaseLetter -> True
207 Char.ModifierLetter -> True
208 Char.OtherLetter -> True
209 Char.TitlecaseLetter -> True
210 Char.UppercaseLetter -> True
214 else W.hcat $ W.strict_text <$> ["\"", t, "\""]
216 width_unit :: Unit -> Int
218 let t = H.unit_text u in
221 (\c -> case Char.generalCategory c of
222 Char.CurrencySymbol -> True
223 Char.LowercaseLetter -> True
224 Char.ModifierLetter -> True
225 Char.OtherLetter -> True
226 Char.TitlecaseLetter -> True
227 Char.UppercaseLetter -> True
232 -- * Write 'Quantity'
233 write_quantity :: Styled_Amount Quantity -> Doc
236 { style_amount_fractioning
237 , style_amount_grouping_integral
238 , style_amount_grouping_fractional
241 let Decimal e n = qty
242 let num = show $ abs $ n
243 let sign = W.bold $ W.yellow $ W.strict_text (if n < 0 then "-" else "")
245 then sign <> W.bold (W.blue $ W.strict_text $ Text.pack num)
247 let num_len = List.length num
250 [ List.replicate (fromIntegral e + 1 - num_len) '0'
252 -- , replicate (fromIntegral precision - fromIntegral e) '0'
254 let (int, frac) = List.splitAt (max 1 (num_len - fromIntegral e)) padded
255 let default_fractioning =
257 del_grouping_sep style_amount_grouping_integral $
258 del_grouping_sep style_amount_grouping_fractional $
262 W.text (TL.pack $ S.maybe id
263 (\g -> List.reverse . group g . List.reverse)
264 style_amount_grouping_integral $ int) <>
265 W.yellow (W.char (S.fromMaybe default_fractioning style_amount_fractioning)) <>
266 W.text (TL.pack $ S.maybe id group style_amount_grouping_fractional frac))
268 group :: Style_Amount_Grouping -> [Char] -> [Char]
269 group (Style_Amount_Grouping sep sizes_) =
270 List.concat . List.reverse .
271 List.map List.reverse . fst .
273 (flip (\digit x -> case x of
274 ([], sizes) -> ([[digit]], sizes)
275 (digits:groups, []) -> ((digit:digits):groups, [])
276 (digits:groups, curr_sizes@(size:sizes)) ->
277 if List.length digits < size
278 then ( (digit:digits):groups, curr_sizes)
279 else ([digit]:[sep]:digits:groups, if List.null sizes then curr_sizes else sizes)
282 del_grouping_sep grouping =
284 S.Just (Style_Amount_Grouping sep _) -> List.delete sep
287 width_quantity :: Style_Amount -> Quantity -> Int
288 width_quantity Style_Amount
289 { style_amount_grouping_integral
290 , style_amount_grouping_fractional
292 let Decimal e n = qty in
293 let sign_len = if n < 0 then 1 else 0 in
294 let fractioning_len = if e > 0 then 1 else 0 in
295 let num_len = if n == 0 then 0 else (1 +) $ I# (integerLogBase# 10 (abs n)) in
296 let pad_left_len = max 0 (fromIntegral e + 1 - num_len) in
297 -- let pad_right_len = max 0 (fromIntegral precision - fromIntegral e) in
298 let padded_len = pad_left_len + num_len {-+ pad_right_len-} in
299 let int_len = max 1 (num_len - fromIntegral e) in
300 let frac_len = max 0 (padded_len - int_len) in
304 + S.maybe 0 (group int_len) style_amount_grouping_integral
305 + S.maybe 0 (group frac_len) style_amount_grouping_fractional
308 group :: Int -> Style_Amount_Grouping -> Int
309 group num_len (Style_Amount_Grouping _sep sizes_) =
312 else loop 0 num_len sizes_
314 loop :: Int -> Int -> [Int] -> Int
319 let l = len - size in
321 else loop (pad + 1) l sizes
323 let l = len - size in
325 else loop (pad + 1) l sizes
328 write_comment :: Comment -> Doc
329 write_comment (Comment com) =
331 W.char char_comment_prefix
332 <> (case Text.uncons com of
333 Just (c, _) | not $ Char.isSpace c -> W.space
337 write_comments :: Doc -> [Comment] -> Doc
338 write_comments prefix =
340 List.intersperse W.line .
341 List.map (\c -> prefix <> write_comment c)
350 , posting_account_ref
352 , posting_comments=cmts
357 let (doc_acct, wi_acct) =
358 case posting_account_ref of
359 S.Just (a S.:!: sa) | context_write_account_ref ctx ->
360 ( write_account_ref a <> S.maybe W.empty write_account sa
361 , width_account_ref a + S.maybe 0 width_account sa )
363 ( write_account posting_account
364 , width_account posting_account ) in
365 (case posting_amounts of
366 Amounts amts | Map.null amts -> doc_acct
370 let amt = styled_amount (context_write_amounts ctx) $ Amount unit qty in
371 let wi_amt = width_amount amt in
373 (if W.is_empty doc then W.empty else W.line <> W.string " ") <>
375 W.fill (context_write_max_posting_width ctx - (wi_acct + wi_amt)) W.space <>
380 [c] -> W.space <> write_comment c
381 _ -> W.line <> write_comments (W.text " ") cmts)
383 -- ** Type 'Widths_Posting'
384 type Widths_Posting = Int
390 widths_postings ctx (Postings ps) =
392 ((case posting_account_ref p of
393 S.Just (a S.:!: sa) | context_write_account_ref ctx ->
394 width_account_ref a +
395 S.maybe 0 width_account sa
396 _ -> width_account (posting_account p)
398 (\len -> if len > 0 then 1 + len else len) $
402 styled_amount (context_write_amounts ctx) $
404 0 (unAmounts $ posting_amounts p)
408 -- * Write 'Transaction'
411 -> Transaction -> Doc
412 write_transaction ctx
414 { transaction_comments
416 , transaction_wording = Wording transaction_wording
417 , transaction_postings = Postings transaction_postings
418 , transaction_tags = Transaction_Tags (Tags tags)
420 let ctx' = ctx { context_write_max_posting_width =
421 let wi = context_write_max_posting_width ctx in
423 then widths_transaction ctx t
427 (W.char char_transaction_date_sep)
428 (write_date <$> NonNull.toNullable transaction_dates)) <>
429 (case transaction_wording of
431 _ -> W.space <> W.dullmagenta (W.strict_text transaction_wording)) <>
433 (case transaction_comments of
435 _ -> write_comments W.space transaction_comments <> W.line) <>
436 TreeMap.foldr_with_Path
438 foldr (\value -> (<>) (W.string " " <>
439 write_transaction_tag (Transaction_Tag (Tag (Tag_Path path) value)) <> W.line)))
442 (W.vsep . (write_posting ctx' <$>))
448 -> j Transaction -> Doc
449 write_transactions ctx j =
450 let ctx' = ctx{context_write_max_posting_width =
451 foldr (max . widths_transaction ctx) 0 j} in
453 write_transaction ctx' t <>
454 (if W.is_empty doc then W.empty else W.line <> W.line <> doc)
457 -- ** Type 'Widths_Transaction'
458 type Widths_Transaction = Widths_Posting
464 widths_transaction ctx
466 { transaction_postings
469 (max . widths_postings ctx)
470 0 [ transaction_postings ]
472 -- ** Write 'Transaction_Tag'
473 write_transaction_tag :: Transaction_Tag -> Doc
474 write_transaction_tag (Transaction_Tag (Tag (Tag_Path path) (Tag_Data value))) =
476 (:) (W.bold $ W.dullyellow $ W.char char_tag_prefix) $
478 (op $ W.char char_tag_sep)
479 (write_transaction_tag_section <$> NonNull.toNullable path)) <>
483 op (W.char char_tag_data_prefix) <>
486 op = W.bold . W.yellow
488 write_transaction_tag_section :: Name -> Doc
489 write_transaction_tag_section = W.bold . W.strict_text . unName
495 -> Journal (j Transaction) -> Doc
496 write_journal ctx jnl =
497 write_transactions ctx $
501 write_chart :: Chart -> Doc
503 TreeMap.foldl_with_Path
504 (\doc acct (Account_Tags (Tags ca)) ->
506 write_account (H.get acct) <> W.line <>
507 TreeMap.foldl_with_Path
512 ddd <> W.string " " <>
513 write_account_tag (Account_Tag (Tag (Tag_Path tp) tv)) <>
522 -- Type 'Context_Write'
525 { context_write_account_ref :: Bool
526 , context_write_amounts :: Style_Amounts
527 , context_write_max_posting_width :: Int
530 context_write :: Context_Write
533 { context_write_account_ref = True
534 , context_write_amounts = Style_Amounts Map.empty
535 , context_write_max_posting_width = 0
539 type Style_Anchor = Bool
540 type instance MC.CanDo (S.ReaderT Context_Write m) (MC.EffReader Context_Write) = 'True
541 instance Monad m => MC.MonadReaderN 'MC.Zero Context_Write (S.ReaderT Context_Write m) where
542 askN _px = S.ReaderT R.ask
543 type instance MC.CanDo (S.ReaderT Context_Write m) (MC.EffReader Style_Anchor) = 'True
544 instance Monad m => MC.MonadReaderN 'MC.Zero Style_Anchor (S.ReaderT Context_Write m) where
545 askN _px = S.ReaderT $ R.asks $ Style_Anchor . context_write_account_ref
548 -- * Type 'Style_Write'
551 { style_write_align :: Bool
552 , style_write_color :: Bool
554 style_write :: Style_Write
557 { style_write_align = True
558 , style_write_color = True
562 write :: Style_Write -> Doc -> TL.Text
565 , style_write_align } =
568 then W.renderPretty style_write_color 1.0 maxBound
569 else W.renderCompact style_write_color
571 writeIO :: Style_Write -> Doc -> Handle -> IO ()
578 then W.renderPretty style_write_color 1.0 maxBound doc
579 else W.renderCompact style_write_color doc