1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE MagicHash #-}
4 {-# LANGUAGE NamedFieldPuns #-}
5 {-# LANGUAGE OverloadedStrings #-}
6 {-# LANGUAGE ScopedTypeVariables #-}
7 {-# LANGUAGE TypeFamilies #-}
8 module Hcompta.Format.JCC.Write where
11 import Data.Char (Char)
12 import qualified Data.Char as Char
13 import Data.Eq (Eq(..))
14 import Data.Maybe (Maybe(..), maybe, fromMaybe)
15 import Data.Foldable (Foldable(..))
16 import Data.Function (($), (.), flip, id)
17 import Data.Functor (Functor(..), (<$>))
18 import qualified Data.Functor.Compose
19 import qualified Data.List as List
20 import qualified Data.List.NonEmpty as NonEmpty
21 import Data.Map.Strict (Map)
22 import qualified Data.Map.Strict as Map
23 import Data.Monoid (Monoid(..))
24 import Data.Ord (Ord(..))
25 import Data.Tuple (fst)
27 import qualified Data.Text as Text
28 import qualified Data.Text.Lazy as TL
29 import qualified Data.Time.LocalTime as Time
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)
36 import Hcompta.Account (Account_Tag(..), Account_Tags(..), Account_Anchor(..))
37 import qualified Hcompta.Account as Account
38 import qualified Hcompta.Anchor as Anchor
39 import Hcompta.Anchor (Anchors(..))
40 import qualified Hcompta.Amount as Amount
41 import qualified Hcompta.Chart as Chart
42 import qualified Hcompta.Unit as Unit
43 import Hcompta.Date (Date)
44 import qualified Hcompta.Date as Date
45 import Hcompta.Lib.Leijen (Doc, (<>))
46 import qualified Hcompta.Lib.Leijen as W
47 import qualified Hcompta.Lib.TreeMap as TreeMap
48 import Hcompta.Tag (Tags(..))
49 import qualified Hcompta.Tag as Tag
50 import Hcompta.Transaction ( Transaction_Anchor(..)
51 , Transaction_Anchors(..)
53 , Transaction_Tags(..)
56 import Hcompta.Format.JCC
57 import Hcompta.Format.JCC.Read
61 write_date :: Date -> Doc
63 let (y, mo, d) = Date.gregorian dat
64 (if y == 0 then W.empty else W.integer y <> sep read_date_ymd_sep) <> do
66 sep read_date_ymd_sep <> int2 d <> do
68 Time.TimeOfDay 0 0 0 -> W.empty
69 Time.TimeOfDay h m s ->
70 sep '_' <> int2 h <> do
71 sep ':' <> int2 m <> do
75 (if s < 10 then W.char '0' else W.empty) <> do
76 W.strict_text $ Text.pack $ show $ (truncate s::Integer)))
79 -- _ | tz_name /= "" -> W.space <> do W.strict_text $ Text.pack tz_name
80 -- _ -> W.space <> do W.strict_text $ Text.pack $ Time.timeZoneOffsetString tz)
83 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 write_date_length :: Date -> Int
88 write_date_length dat = do
89 let (y, _, _) = Date.gregorian dat
93 (if y < 0 then 1 else 0) -- sign
94 + (1 + (I# (integerLogBase# 10 (abs y)))) -- year
99 + (case Date.tod dat of
100 Time.TimeOfDay 0 0 0 -> 0
101 Time.TimeOfDay _ _ s ->
114 write_account :: Account -> Doc
117 (W.bold $ W.dullblack $ W.char read_account_section_sep) <>
118 write_account_section a
120 write_account_section :: Account.Account_Section Account -> Doc
121 write_account_section = W.strict_text
123 write_account_length :: Account -> Int
124 write_account_length acct =
126 (\acc -> (1 +) . (acc +) . Text.length)
129 -- ** Write 'Account_Anchor'
131 write_account_anchor :: Account_Anchor -> Doc
132 write_account_anchor (Account_Anchor anchor) =
134 (:) (op $ W.char read_account_anchor_prefix) $
137 (op $ W.char read_account_anchor_sep)
138 (W.strict_text <$> anchor)
139 where op = W.bold . W.dullyellow
141 write_account_anchor_length :: Account_Anchor -> Int
142 write_account_anchor_length (Account_Anchor anch) =
144 (\acc -> (1 +) . (acc +) . Text.length)
147 -- ** Write 'Account_Tag'
149 write_account_tag :: Account_Tag -> Doc
150 write_account_tag (Account_Tag (path, value)) =
152 (:) (op $ W.char read_account_tag_prefix) $
155 (op $ W.char read_account_tag_sep)
156 (W.strict_text <$> path)) <>
160 (op $ W.char read_account_tag_value_prefix) <>
162 where op = W.bold . W.dullyellow
166 write_amount :: Amount_Styled Amount -> Doc
169 { amount_style_unit_side
170 , amount_style_unit_spaced
173 let unt = Amount.amount_unit amt in
174 case amount_style_unit_side of
175 Just Amount_Style_Side_Left ->
177 case amount_style_unit_spaced of
178 Just True | unt /= Unit.unit_empty -> W.space
181 <> write_quantity (sty, Amount.amount_quantity amt)
182 <> case amount_style_unit_side of
183 (Just Amount_Style_Side_Right) ->
184 (case amount_style_unit_spaced of
185 Just True | unt /= Unit.unit_empty -> W.space
189 (case amount_style_unit_spaced of
190 Just True | unt /= Unit.unit_empty -> W.space
195 write_amount_length :: Amount_Styled Amount -> Int
196 write_amount_length (sty@(Amount_Style { amount_style_unit_spaced }), amt) =
197 let unt = Amount.amount_unit amt in
198 write_unit_length unt
199 + (case amount_style_unit_spaced of
200 { Just True | unt /= Unit.unit_empty -> 1; _ -> 0 })
201 + write_quantity_length sty (Amount.amount_quantity amt)
203 -- ** Write 'Amount's
205 write_amounts :: Amount_Styles -> Map Unit Quantity -> Doc
206 write_amounts styles =
211 else doc <> W.space <>
212 (W.bold $ W.yellow $ W.char read_amount_sep) <>
215 amount_styled styles $
219 write_amounts_length :: Amount_Styles -> Map Unit Quantity -> Int
220 write_amounts_length styles amts =
225 (\unit qty -> (3 +) . (+)
226 (write_amount_length $
227 amount_styled styles $
233 write_unit :: Unit -> Doc
235 let t = Unit.unit_text u in
238 (\c -> case Char.generalCategory c of
239 Char.CurrencySymbol -> True
240 Char.LowercaseLetter -> True
241 Char.ModifierLetter -> True
242 Char.OtherLetter -> True
243 Char.TitlecaseLetter -> True
244 Char.UppercaseLetter -> True
248 else W.hcat $ W.strict_text <$> ["\"", t, "\""]
250 write_unit_length :: Unit -> Int
251 write_unit_length u =
252 let t = Unit.unit_text u in
255 (\c -> case Char.generalCategory c of
256 Char.CurrencySymbol -> True
257 Char.LowercaseLetter -> True
258 Char.ModifierLetter -> True
259 Char.OtherLetter -> True
260 Char.TitlecaseLetter -> True
261 Char.UppercaseLetter -> True
266 -- * Write 'Quantity'
268 write_quantity :: Amount_Styled Quantity -> Doc
271 { amount_style_fractioning
272 , amount_style_grouping_integral
273 , amount_style_grouping_fractional
276 let Decimal e n = qty
277 let num = show $ abs $ n
278 let sign = W.bold $ W.yellow $ W.strict_text (if n < 0 then "-" else "")
280 True -> sign <> do W.bold $ W.blue $ (W.strict_text $ Text.pack num)
282 let num_len = List.length num
285 [ List.replicate (fromIntegral e + 1 - num_len) '0'
287 -- , replicate (fromIntegral precision - fromIntegral e) '0'
289 let (int, frac) = List.splitAt (max 1 (num_len - fromIntegral e)) padded
290 let default_fractioning =
292 del_grouping_sep amount_style_grouping_integral $
293 del_grouping_sep amount_style_grouping_fractional $
297 W.text (TL.pack $ maybe id
298 (\g -> List.reverse . group g . List.reverse)
299 amount_style_grouping_integral $ int) <> do
300 (W.yellow $ W.char (fromMaybe default_fractioning amount_style_fractioning)) <> do
301 W.text (TL.pack $ maybe id group amount_style_grouping_fractional frac)
303 group :: Amount_Style_Grouping -> [Char] -> [Char]
304 group (Amount_Style_Grouping sep sizes_) =
305 List.concat . List.reverse .
306 List.map List.reverse . fst .
308 (flip (\digit x -> case x of
309 ([], sizes) -> ([[digit]], sizes)
310 (digits:groups, []) -> ((digit:digits):groups, [])
311 (digits:groups, curr_sizes@(size:sizes)) ->
312 if List.length digits < size
313 then ( (digit:digits):groups, curr_sizes)
314 else ([digit]:[sep]:digits:groups, if List.null sizes then curr_sizes else sizes)
317 del_grouping_sep grouping =
319 Just (Amount_Style_Grouping sep _) -> List.delete sep
322 write_quantity_length :: Amount_Style -> Quantity -> Int
323 write_quantity_length Amount_Style
324 { amount_style_grouping_integral
325 , amount_style_grouping_fractional
327 let Decimal e n = qty in
328 let sign_len = if n < 0 then 1 else 0 in
329 let fractioning_len = if e > 0 then 1 else 0 in
330 let num_len = if n == 0 then 0 else (1 +) $ I# (integerLogBase# 10 (abs n)) in
331 let pad_left_len = max 0 (fromIntegral e + 1 - num_len) in
332 -- let pad_right_len = max 0 (fromIntegral precision - fromIntegral e) in
333 let padded_len = pad_left_len + num_len {-+ pad_right_len-} in
334 let int_len = max 1 (num_len - fromIntegral e) in
335 let frac_len = max 0 (padded_len - int_len) in
339 + maybe 0 (group int_len) amount_style_grouping_integral
340 + maybe 0 (group frac_len) amount_style_grouping_fractional
343 group :: Int -> Amount_Style_Grouping -> Int
344 group num_len (Amount_Style_Grouping _sep sizes_) =
347 else loop 0 num_len sizes_
349 loop :: Int -> Int -> [Int] -> Int
354 let l = len - size in
356 else loop (pad + 1) l sizes
358 let l = len - size in
360 else loop (pad + 1) l sizes
364 write_comment :: Comment -> Doc
367 W.char read_comment_prefix
368 <> (case Text.uncons com of
369 Just (c, _) | not $ Char.isSpace c -> W.space
373 write_comments :: Doc -> [Comment] -> Doc
374 write_comments prefix =
376 List.intersperse W.line .
377 List.map (\c -> prefix <> write_comment c)
381 write_posting :: Amount_Styles -> Posting_Lengths -> Posting -> Doc
382 write_posting styles max_posting_length
385 , posting_account_anchor
387 , posting_comments=cmts
392 let (doc_acct, len_acct) =
393 case posting_account_anchor of
395 ( write_account posting_account
396 , write_account_length posting_account )
398 ( write_account_anchor a <> maybe W.empty write_account sa
399 , write_account_anchor_length a + maybe 0 write_account_length sa )
400 case Map.null posting_amounts of
403 let len_amts = write_amounts_length styles posting_amounts in
405 W.fill (1 + max_posting_length - (len_acct + len_amts)) W.space <>
406 write_amounts styles posting_amounts
409 [c] -> W.space <> write_comment c
410 _ -> W.line <> do write_comments (W.text " ") cmts)
412 -- ** Type 'Posting_Lengths'
414 type Posting_Lengths = (Int)
416 write_postings_lengths
418 -> Map Account [Posting]
421 write_postings_lengths styles ps pl =
424 ( write_account_length (posting_account p)
425 + write_amounts_length styles (posting_amounts p) )
427 (Data.Functor.Compose.Compose ps)
429 -- * Write 'Transaction'
431 write_transaction :: Amount_Styles -> Transaction -> Doc
432 write_transaction styles t =
433 write_transaction_with_lengths
434 styles (write_transaction_lengths styles t 0) t
436 write_transactions :: Foldable j => Amount_Styles -> j Transaction -> Doc
437 write_transactions styles j = do
438 let transaction_lengths_ =
439 foldr (write_transaction_lengths styles) 0 j
441 write_transaction_with_lengths styles transaction_lengths_ t <>
442 (if W.is_empty doc then W.empty else W.line <> doc)
445 write_transaction_with_lengths
447 -> Transaction_Lengths
448 -> Transaction -> Doc
449 write_transaction_with_lengths
453 { transaction_comments
454 , transaction_dates=(first_date, dates)
455 , transaction_wording
456 , transaction_postings
457 , transaction_anchors=Transaction_Anchors (Anchors anchors)
458 , transaction_tags=Transaction_Tags (Tags tags)
462 (W.char read_transaction_date_sep)
463 (write_date <$> (first_date:dates))) <> do
464 (case transaction_wording of
466 _ -> W.space <> (W.dullmagenta $ W.strict_text transaction_wording)) <> do
468 (case transaction_comments of
470 _ -> write_comments W.space transaction_comments <> W.line) <> do
472 (\path () -> ((W.string " " <>
473 write_transaction_anchor (Transaction_Anchor path) <> W.line) <>))
474 W.empty anchors <> do
477 foldr (\value -> (<>) (W.string " " <>
478 write_transaction_tag (Transaction_Tag (path, value)) <> W.line)))
481 (W.vsep . fmap (write_posting styles posting_lengths_))
482 transaction_postings <> W.line
484 -- ** Type 'Transaction_Lengths'
486 type Transaction_Lengths = Posting_Lengths
488 write_transaction_lengths
493 write_transaction_lengths
496 { transaction_postings
497 } posting_lengths = do
499 (flip $ write_postings_lengths styles)
501 [ transaction_postings ]
503 -- ** Write 'Transaction_Tag'
505 write_transaction_tag :: Transaction_Tag -> Doc
506 write_transaction_tag (Transaction_Tag (path, value)) =
508 (:) (W.bold $ W.dullyellow $ W.char read_transaction_tag_prefix) $
511 (op $ W.char read_transaction_tag_sep)
512 (write_transaction_tag_section <$> path)) <>
516 (op $ W.char read_transaction_tag_value_prefix) <>
519 op = W.bold . W.yellow
521 write_transaction_tag_section :: Tag.Section -> Doc
522 write_transaction_tag_section = W.bold . W.strict_text
524 -- ** Write 'Transaction_Anchor'
526 write_transaction_anchor :: Transaction_Anchor -> Doc
527 write_transaction_anchor (Transaction_Anchor path) =
529 (:) (op $ W.char read_transaction_anchor_prefix) $
532 (op $ W.char read_transaction_anchor_sep)
533 (write_transaction_anchor_section <$> path)
535 op = W.bold . W.yellow
537 write_transaction_anchor_section :: Anchor.Section -> Doc
538 write_transaction_anchor_section = W.bold . W.strict_text
544 , Monoid (j Transaction)
545 ) => Journal (j Transaction) -> Doc
546 write_journal Journal
547 { journal_amount_styles
549 } = write_transactions journal_amount_styles journal_content
553 write_chart :: Chart -> Doc
555 TreeMap.foldl_with_Path
556 (\doc acct (Account_Tags (Tags ca)) ->
558 write_account acct <> W.line <>
564 ddd <> W.string " " <>
565 write_account_tag (Account_Tag (tn, tv)) <>
574 -- * Type 'Write_Style'
578 { write_style_align :: Bool
579 , write_style_color :: Bool
581 write_style :: Write_Style
584 { write_style_align = True
585 , write_style_color = True
589 write :: Write_Style -> Doc -> TL.Text
592 , write_style_align } =
595 then W.renderPretty write_style_color 1.0 maxBound
596 else W.renderCompact write_style_color
598 writeIO :: Write_Style -> Doc -> Handle -> IO ()
605 then W.renderPretty write_style_color 1.0 maxBound doc
606 else W.renderCompact write_style_color doc