1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE MagicHash #-}
4 {-# LANGUAGE NamedFieldPuns #-}
5 {-# LANGUAGE OverloadedStrings #-}
6 {-# LANGUAGE ScopedTypeVariables #-}
7 {-# LANGUAGE TypeFamilies #-}
8 module Hcompta.Ledger.Write where
10 import Prelude (Bounded(..), Integer, Num(..), RealFrac(..), Show(..), fromIntegral)
11 import Control.Applicative (Applicative(..), (<*))
13 import Data.Char (Char, isSpace)
14 import qualified Data.Char as Char
16 import Data.Either (Either(..))
17 import Data.Eq (Eq(..))
18 import GHC.Exts (Int(..))
19 import qualified Data.Foldable
20 import Data.Foldable (Foldable(..))
21 import Data.Function (($), (.), flip, id)
22 import Data.Functor (Functor(..), (<$>))
23 import qualified Data.Functor.Compose
24 import System.IO (IO, Handle)
25 import GHC.Integer.Logarithms (integerLogBase#)
26 import Data.List ((++))
27 import qualified Data.List as List
28 import qualified Data.List.NonEmpty
29 import Data.Map.Strict (Map)
30 import qualified Data.Map.Strict as Map
31 import Data.Maybe (Maybe(..), maybe, fromMaybe)
32 import Control.Monad (Monad(..))
33 import Data.Monoid (Monoid(..), (<>))
34 import Data.Ord (Ord(..))
35 import Text.Parsec (Stream, ParsecT)
36 import qualified Text.Parsec as R hiding (satisfy, char)
37 import qualified Text.Parsec.Combinator.CorrectSourcePosWithTab as R
38 import qualified Data.Text as Text
39 import qualified Data.Text.Lazy as TL
40 import qualified Data.Time.LocalTime as Time
41 import qualified Data.TreeMap.Strict as TreeMap
42 import Data.Tuple (fst)
45 import qualified Hcompta as H
46 import Text.WalderLeijen.ANSI.Text (Doc)
47 import qualified Text.WalderLeijen.ANSI.Text as W
49 import Hcompta.Ledger.Account
50 import Hcompta.Ledger.Amount
51 import Hcompta.Ledger.Chart
52 import Hcompta.Ledger.Posting
53 import Hcompta.Ledger.Transaction
54 import Hcompta.Ledger.Journal
55 import Hcompta.Ledger.Read
59 write_date :: H.Date -> Doc
61 let (y, mo, d) = H.date_gregorian dat in
62 (if y == 0 then W.empty else W.integer y <> sep '-') <>
65 (case H.date_tod dat of
66 Time.TimeOfDay 0 0 0 -> W.empty
67 Time.TimeOfDay h m s ->
73 (if s < 10 then W.char '0' else W.empty) <>
74 W.strict_text (Text.pack $ show $ (truncate s::Integer))))
77 -- _ | tz_name /= "" -> W.space <> do W.strict_text $ Text.pack tz_name
78 -- _ -> W.space <> do W.strict_text $ Text.pack $ Time.timeZoneOffsetString tz)
81 int2 i = if i < 10 then W.char '0' <> W.int i else W.int i
83 sep = W.bold . W.dullblack . W.char
85 write_date_length :: H.Date -> Int
86 write_date_length dat = do
87 let (y, _, _) = H.date_gregorian dat
91 (if y < 0 then 1 else 0) -- sign
92 + (1 + (I# (integerLogBase# 10 (abs y)))) -- year
97 + (case H.date_tod dat of
98 Time.TimeOfDay 0 0 0 -> 0
99 Time.TimeOfDay _ _ s ->
112 write_account :: Posting_Type -> Account -> Doc
113 write_account type_ =
115 Posting_Type_Regular -> account_
116 Posting_Type_Virtual -> \acct ->
117 W.char read_posting_type_virtual_begin <>
119 W.char read_posting_type_virtual_end
120 Posting_Type_Virtual_Balanced -> \acct ->
121 W.char read_posting_type_virtual_balanced_begin <>
123 W.char read_posting_type_virtual_balanced_end
125 account_ :: Account -> Doc
128 Data.List.NonEmpty.toList $
129 Data.List.NonEmpty.intersperse
130 (W.bold $ W.dullblack $ W.char read_account_section_sep)
131 (Data.List.NonEmpty.map write_account_section acct)
133 write_account_section :: Account_Section -> Doc
134 write_account_section = W.strict_text
136 write_account_length :: Posting_Type -> Account -> Int
137 write_account_length type_ acct =
139 (\acc -> (1 +) . (acc +) . Text.length)
142 Posting_Type_Regular -> 0
143 Posting_Type_Virtual -> 2
144 Posting_Type_Virtual_Balanced -> 2
148 write_amount :: Amount_Styled Amount -> Doc
151 { amount_style_unit_side
152 , amount_style_unit_spaced
155 let unt = H.amount_unit amt in
156 case amount_style_unit_side of
157 Just Amount_Style_Side_Left ->
159 case amount_style_unit_spaced of
160 Just True | unt /= H.unit_empty -> W.space
163 <> write_quantity (sty, H.amount_quantity amt)
164 <> case amount_style_unit_side of
165 (Just Amount_Style_Side_Right) ->
166 (case amount_style_unit_spaced of
167 Just True | unt /= H.unit_empty -> W.space
171 (case amount_style_unit_spaced of
172 Just True | unt /= H.unit_empty -> W.space
177 write_amount_length :: Amount_Styled Amount -> Int
178 write_amount_length (sty@(Amount_Style { amount_style_unit_spaced }), amt) =
179 let unt = H.amount_unit amt in
180 write_unit_length unt
181 + (case amount_style_unit_spaced of
182 { Just True | unt /= H.unit_empty -> 1; _ -> 0 })
183 + write_quantity_length sty (H.amount_quantity amt)
185 -- ** Write 'Amount's
187 write_amounts :: Amount_Styles -> Map Unit Quantity -> Doc
188 write_amounts styles =
193 else doc <> W.space <>
194 W.bold (W.yellow $ W.char read_amount_sep) <>
196 write_amount (amount_styled styles $ Amount unit qty))
199 write_amounts_length :: Amount_Styles -> Map Unit Quantity -> Int
200 write_amounts_length styles amts =
205 (\unit qty -> (3 +) . (+)
206 (write_amount_length $
207 amount_styled styles $
213 write_unit :: Unit -> Doc
215 let t = H.unit_text u in
218 (\c -> case Char.generalCategory c of
219 Char.CurrencySymbol -> True
220 Char.LowercaseLetter -> True
221 Char.ModifierLetter -> True
222 Char.OtherLetter -> True
223 Char.TitlecaseLetter -> True
224 Char.UppercaseLetter -> True
228 else W.hcat $ W.strict_text <$> ["\"", t, "\""]
230 write_unit_length :: Unit -> Int
231 write_unit_length u =
232 let t = H.unit_text u in
235 (\c -> case Char.generalCategory c of
236 Char.CurrencySymbol -> True
237 Char.LowercaseLetter -> True
238 Char.ModifierLetter -> True
239 Char.OtherLetter -> True
240 Char.TitlecaseLetter -> True
241 Char.UppercaseLetter -> True
246 -- * Write 'Quantity'
248 write_quantity :: Amount_Styled Quantity -> Doc
251 { amount_style_fractioning
252 , amount_style_grouping_integral
253 , amount_style_grouping_fractional
256 let Decimal e n = qty
257 let num = show $ abs $ n
258 let sign = W.bold $ W.yellow $ W.strict_text (if n < 0 then "-" else "")
260 then sign <> W.bold (W.blue $ W.strict_text $ Text.pack num)
262 let num_len = List.length num in
265 [ List.replicate (fromIntegral e + 1 - num_len) '0'
267 -- , replicate (fromIntegral precision - fromIntegral e) '0'
269 let (int, frac) = List.splitAt (max 1 (num_len - fromIntegral e)) padded in
270 let default_fractioning =
272 del_grouping_sep amount_style_grouping_integral $
273 del_grouping_sep amount_style_grouping_fractional $
277 W.text (TL.pack $ maybe id
278 (\g -> List.reverse . group g . List.reverse)
279 amount_style_grouping_integral $ int) <>
280 W.yellow (W.char (fromMaybe default_fractioning amount_style_fractioning)) <>
281 W.text (TL.pack $ maybe id group amount_style_grouping_fractional frac))
283 group :: Amount_Style_Grouping -> [Char] -> [Char]
284 group (Amount_Style_Grouping sep sizes_) =
285 List.concat . List.reverse .
286 List.map List.reverse . fst .
288 (flip (\digit x -> case x of
289 ([], sizes) -> ([[digit]], sizes)
290 (digits:groups, []) -> ((digit:digits):groups, [])
291 (digits:groups, curr_sizes@(size:sizes)) ->
292 if List.length digits < size
293 then ( (digit:digits):groups, curr_sizes)
294 else ([digit]:[sep]:digits:groups, if List.null sizes then curr_sizes else sizes)
297 del_grouping_sep grouping =
299 Just (Amount_Style_Grouping sep _) -> List.delete sep
302 write_quantity_length :: Amount_Style -> Quantity -> Int
303 write_quantity_length Amount_Style
304 { amount_style_grouping_integral
305 , amount_style_grouping_fractional
307 let Decimal e n = qty in
308 let sign_len = if n < 0 then 1 else 0 in
309 let fractioning_len = if e > 0 then 1 else 0 in
310 let num_len = if n == 0 then 0 else (1 +) $ I# (integerLogBase# 10 (abs n)) in
311 let pad_left_len = max 0 (fromIntegral e + 1 - num_len) in
312 -- let pad_right_len = max 0 (fromIntegral precision - fromIntegral e) in
313 let padded_len = pad_left_len + num_len {-+ pad_right_len-} in
314 let int_len = max 1 (num_len - fromIntegral e) in
315 let frac_len = max 0 (padded_len - int_len) in
319 + maybe 0 (group int_len) amount_style_grouping_integral
320 + maybe 0 (group frac_len) amount_style_grouping_fractional
323 group :: Int -> Amount_Style_Grouping -> Int
324 group num_len (Amount_Style_Grouping _sep sizes_) =
327 else loop 0 num_len sizes_
329 loop :: Int -> Int -> [Int] -> Int
334 let l = len - size in
336 else loop (pad + 1) l sizes
338 let l = len - size in
340 else loop (pad + 1) l sizes
344 write_comment :: Comment -> Doc
347 W.char read_comment_prefix
348 <> (case Text.uncons com of
349 Just (c, _) | not $ Data.Char.isSpace c -> W.space
351 <> W.if_color colorize (W.strict_text com)
356 pre <- R.many $ R.try $ do
357 ns <- R.many $ R.satisfy
358 (\c -> c /= read_tag_value_sep
359 && not (Data.Char.isSpace c))
360 sh <- R.spaceHorizontal
362 ((W.text $ TL.pack $ mconcat pre) <>) <$> tags <* R.eof)
364 Left _ -> W.strict_text com
366 tags :: Stream s m Char => ParsecT s u m Doc
370 <*> (W.hcat <$> R.many (R.try (tag_sep >>= (\s -> (s <>) <$> tag_))))
372 tag_sep :: Stream s m Char => ParsecT s u m Doc
374 s <- R.char read_tag_sep
375 sh <- R.many R.spaceHorizontal
379 tag_ :: Stream s m Char => ParsecT s u m Doc
383 foldMap (\s -> W.dullyellow (W.strict_text s) <>
384 W.bold (W.dullblack $ W.char read_tag_value_sep)) p <>
385 W.red (W.strict_text v)
387 write_comments :: Doc -> [Comment] -> Doc
388 write_comments prefix =
390 List.intersperse W.line .
391 List.map (\c -> prefix <> write_comment c)
395 write_tag :: H.Tag -> Doc
398 W.dullyellow (W.strict_text s) <>
399 W.char read_tag_value_sep) p <>
400 W.dullred (W.strict_text v)
404 write_posting :: Amount_Styles -> Posting_Lengths -> Posting -> Doc
405 write_posting styles max_posting_length
414 let type_ = posting_type p in
416 write_status posting_status <>
417 if Map.null posting_amounts
418 then write_account type_ posting_account
420 let len_acct = write_account_length type_ posting_account in
421 let len_amts = write_amounts_length styles posting_amounts in
422 write_account type_ posting_account <>
423 W.fill (2 + max_posting_length - (len_acct + len_amts)) (W.space <> W.space) <>
424 write_amounts styles posting_amounts
425 <> (case posting_comments of
427 [c] -> W.space <> write_comment c
428 _ -> W.line <> write_comments (write_indent <> W.space) posting_comments)
431 write_indent = W.space <> W.space
433 write_status :: Status -> Doc
439 -- ** Type 'Posting_Lengths'
441 type Posting_Lengths = (Int)
443 write_postings_lengths
445 -> Map Account [Posting]
448 write_postings_lengths styles ps pl =
452 ( write_account_length (posting_type p) (posting_account p)
453 + write_amounts_length styles (posting_amounts p) )
455 (Data.Functor.Compose.Compose ps)
457 -- * Write 'Transaction'
459 write_transaction :: Amount_Styles -> Transaction -> Doc
460 write_transaction styles t =
461 write_transaction_with_lengths
462 styles (write_transaction_lengths styles t 0) t
464 write_transactions :: Foldable j => Amount_Styles -> j Transaction -> Doc
465 write_transactions styles j = do
466 let transaction_lengths_ =
467 Data.Foldable.foldr (write_transaction_lengths styles) 0 j
470 write_transaction_with_lengths styles transaction_lengths_ t <>
471 (if W.is_empty doc then W.empty else W.line <> doc)
476 write_transaction_with_lengths
478 -> Transaction_Lengths
479 -> Transaction -> Doc
480 write_transaction_with_lengths
485 , transaction_comments_before
486 , transaction_comments_after
487 , transaction_dates=(first_date, dates)
488 , transaction_postings
490 -- , transaction_tags
491 , transaction_wording
493 (case transaction_comments_before of
495 _ -> write_comments W.space transaction_comments_before <> W.line) <>
498 (W.char read_date_ymd_sep)
499 (write_date <$> (first_date:dates))) <>
500 (if transaction_status
501 then W.space <> write_status transaction_status
503 write_code transaction_code <>
504 (case transaction_wording of
506 _ -> W.space <> W.dullmagenta (W.strict_text transaction_wording)) <>
508 (case transaction_comments_after of
510 _ -> write_comments W.space transaction_comments_after <> W.line) <>
512 (W.vsep . fmap (write_posting styles posting_lengths_))
516 write_code :: Code -> Doc
520 t -> W.space <> W.char '(' <> W.strict_text t <> W.char ')'
522 -- ** Type 'Transaction_Lengths'
524 type Transaction_Lengths = Posting_Lengths
526 write_transaction_lengths
531 write_transaction_lengths
534 { transaction_postings
537 (flip $ write_postings_lengths styles)
539 [ transaction_postings ]
545 , Monoid (j Transaction)
546 ) => Journal (j Transaction) -> Doc
547 write_journal Journal
548 { journal_amount_styles
550 } = write_transactions journal_amount_styles journal_content
554 write_chart :: Chart -> Doc
556 TreeMap.foldl_with_Path
557 (\doc acct (H.Account_Tags (H.Tags ca)) ->
559 write_account Posting_Type_Regular acct <> W.line <>
565 ddd <> write_indent <> write_tag (tn, tv) <> W.line)
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