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.Ledger.Write where
10 import Control.Applicative ((<$>), (<*>), (<*))
11 import Control.Monad (Monad(..))
13 import Data.Char (Char, isSpace)
14 import qualified Data.Char as Char
15 import Data.Either (Either(..))
16 import Data.Eq (Eq(..))
17 import Data.Maybe (Maybe(..), maybe, fromMaybe)
18 import qualified Data.Foldable
19 import Data.Foldable (Foldable(..))
20 import Data.Function (($), (.), flip, id)
21 import Data.Functor (Functor(..))
22 import qualified Data.Functor.Compose
23 import Data.List ((++))
24 import qualified Data.List as List
25 import qualified Data.List.NonEmpty
26 import Data.Map.Strict (Map)
27 import qualified Data.Map.Strict as Map
28 import Data.Monoid (Monoid(..))
29 import Data.Ord (Ord(..))
30 import Data.Tuple (fst)
32 import qualified Data.Text as Text
33 import qualified Data.Text.Lazy as TL
34 import qualified Data.Time.LocalTime as Time
35 import GHC.Exts (Int(..))
36 import GHC.Integer.Logarithms (integerLogBase#)
37 import Prelude (Bounded(..), Integer, Num(..), RealFrac(..), Show(..), fromIntegral)
38 import System.IO (IO, Handle)
39 import Text.Parsec (Stream, ParsecT)
40 import qualified Text.Parsec as R hiding (satisfy, char)
43 import Hcompta.Account (Account_Tags(..))
44 import qualified Hcompta.Account as Account
45 import qualified Hcompta.Amount as Amount
46 import qualified Hcompta.Chart as Chart
47 import qualified Hcompta.Unit as Unit
48 import Hcompta.Date (Date)
49 import qualified Hcompta.Date as Date
50 import Hcompta.Lib.Leijen (Doc, (<>))
51 import qualified Hcompta.Lib.Leijen as W
52 import qualified Hcompta.Lib.Parsec as R
53 import qualified Hcompta.Lib.TreeMap as TreeMap
54 import Hcompta.Tag (Tag, Tags(..))
56 import Hcompta.Format.Ledger
57 import Hcompta.Format.Ledger.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 '-') <> do
66 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 :: Posting_Type -> Account -> Doc
115 write_account type_ =
117 Posting_Type_Regular -> account_
118 Posting_Type_Virtual -> \acct ->
119 W.char read_posting_type_virtual_begin <> do
121 W.char read_posting_type_virtual_end
122 Posting_Type_Virtual_Balanced -> \acct ->
123 W.char read_posting_type_virtual_balanced_begin <> do
125 W.char read_posting_type_virtual_balanced_end
127 account_ :: Account -> Doc
130 Data.List.NonEmpty.toList $
131 Data.List.NonEmpty.intersperse
132 (W.bold $ W.dullblack $ W.char read_account_section_sep)
133 (Data.List.NonEmpty.map write_account_section acct)
135 write_account_section :: Account.Account_Section Account -> Doc
136 write_account_section = W.strict_text
138 write_account_length :: Posting_Type -> Account -> Int
139 write_account_length type_ acct =
141 (\acc -> (1 +) . (acc +) . Text.length)
144 Posting_Type_Regular -> 0
145 Posting_Type_Virtual -> 2
146 Posting_Type_Virtual_Balanced -> 2
150 write_amount :: Amount_Styled Amount -> Doc
153 { amount_style_unit_side
154 , amount_style_unit_spaced
157 let unt = Amount.amount_unit amt in
158 case amount_style_unit_side of
159 Just Amount_Style_Side_Left ->
161 case amount_style_unit_spaced of
162 Just True | unt /= Unit.unit_empty -> W.space
165 <> write_quantity (sty, Amount.amount_quantity amt)
166 <> case amount_style_unit_side of
167 (Just Amount_Style_Side_Right) ->
168 (case amount_style_unit_spaced of
169 Just True | unt /= Unit.unit_empty -> W.space
173 (case amount_style_unit_spaced of
174 Just True | unt /= Unit.unit_empty -> W.space
179 write_amount_length :: Amount_Styled Amount -> Int
180 write_amount_length (sty@(Amount_Style { amount_style_unit_spaced }), amt) =
181 let unt = Amount.amount_unit amt in
182 write_unit_length unt
183 + (case amount_style_unit_spaced of
184 { Just True | unt /= Unit.unit_empty -> 1; _ -> 0 })
185 + write_quantity_length sty (Amount.amount_quantity amt)
187 -- ** Write 'Amount's
189 write_amounts :: Amount_Styles -> Map Unit Quantity -> Doc
190 write_amounts styles =
195 else doc <> W.space <>
196 (W.bold $ W.yellow $ W.char read_amount_sep) <>
199 amount_styled styles $
203 write_amounts_length :: Amount_Styles -> Map Unit Quantity -> Int
204 write_amounts_length styles amts =
209 (\unit qty -> (3 +) . (+)
210 (write_amount_length $
211 amount_styled styles $
217 write_unit :: Unit -> Doc
219 let t = Unit.unit_text u in
222 (\c -> case Char.generalCategory c of
223 Char.CurrencySymbol -> True
224 Char.LowercaseLetter -> True
225 Char.ModifierLetter -> True
226 Char.OtherLetter -> True
227 Char.TitlecaseLetter -> True
228 Char.UppercaseLetter -> True
232 else W.hcat $ W.strict_text <$> ["\"", t, "\""]
234 write_unit_length :: Unit -> Int
235 write_unit_length u =
236 let t = Unit.unit_text u in
239 (\c -> case Char.generalCategory c of
240 Char.CurrencySymbol -> True
241 Char.LowercaseLetter -> True
242 Char.ModifierLetter -> True
243 Char.OtherLetter -> True
244 Char.TitlecaseLetter -> True
245 Char.UppercaseLetter -> True
250 -- * Write 'Quantity'
252 write_quantity :: Amount_Styled Quantity -> Doc
255 { amount_style_fractioning
256 , amount_style_grouping_integral
257 , amount_style_grouping_fractional
260 let Decimal e n = qty
261 let num = show $ abs $ n
262 let sign = W.bold $ W.yellow $ W.strict_text (if n < 0 then "-" else "")
264 True -> sign <> do W.bold $ W.blue $ (W.strict_text $ Text.pack num)
266 let num_len = List.length num
269 [ List.replicate (fromIntegral e + 1 - num_len) '0'
271 -- , replicate (fromIntegral precision - fromIntegral e) '0'
273 let (int, frac) = List.splitAt (max 1 (num_len - fromIntegral e)) padded
274 let default_fractioning =
276 del_grouping_sep amount_style_grouping_integral $
277 del_grouping_sep amount_style_grouping_fractional $
281 W.text (TL.pack $ maybe id
282 (\g -> List.reverse . group g . List.reverse)
283 amount_style_grouping_integral $ int) <> do
284 (W.yellow $ W.char (fromMaybe default_fractioning amount_style_fractioning)) <> do
285 W.text (TL.pack $ maybe id group amount_style_grouping_fractional frac)
287 group :: Amount_Style_Grouping -> [Char] -> [Char]
288 group (Amount_Style_Grouping sep sizes_) =
289 List.concat . List.reverse .
290 List.map List.reverse . fst .
292 (flip (\digit x -> case x of
293 ([], sizes) -> ([[digit]], sizes)
294 (digits:groups, []) -> ((digit:digits):groups, [])
295 (digits:groups, curr_sizes@(size:sizes)) ->
296 if List.length digits < size
297 then ( (digit:digits):groups, curr_sizes)
298 else ([digit]:[sep]:digits:groups, if List.null sizes then curr_sizes else sizes)
301 del_grouping_sep grouping =
303 Just (Amount_Style_Grouping sep _) -> List.delete sep
306 write_quantity_length :: Amount_Style -> Quantity -> Int
307 write_quantity_length Amount_Style
308 { amount_style_grouping_integral
309 , amount_style_grouping_fractional
311 let Decimal e n = qty in
312 let sign_len = if n < 0 then 1 else 0 in
313 let fractioning_len = if e > 0 then 1 else 0 in
314 let num_len = if n == 0 then 0 else (1 +) $ I# (integerLogBase# 10 (abs n)) in
315 let pad_left_len = max 0 (fromIntegral e + 1 - num_len) in
316 -- let pad_right_len = max 0 (fromIntegral precision - fromIntegral e) in
317 let padded_len = pad_left_len + num_len {-+ pad_right_len-} in
318 let int_len = max 1 (num_len - fromIntegral e) in
319 let frac_len = max 0 (padded_len - int_len) in
323 + maybe 0 (group int_len) amount_style_grouping_integral
324 + maybe 0 (group frac_len) amount_style_grouping_fractional
327 group :: Int -> Amount_Style_Grouping -> Int
328 group num_len (Amount_Style_Grouping _sep sizes_) =
331 else loop 0 num_len sizes_
333 loop :: Int -> Int -> [Int] -> Int
338 let l = len - size in
340 else loop (pad + 1) l sizes
342 let l = len - size in
344 else loop (pad + 1) l sizes
348 write_comment :: Comment -> Doc
351 W.char read_comment_prefix
352 <> (case Text.uncons com of
353 Just (c, _) | not $ Data.Char.isSpace c -> W.space
355 <> do W.if_color colorize (W.strict_text com)
360 pre <- R.many $ R.try $ do
361 ns <- R.many $ R.satisfy
362 (\c -> c /= read_tag_value_sep
363 && not (Data.Char.isSpace c))
364 sh <- R.space_horizontal
366 ((W.text $ TL.pack $ mconcat pre) <>) <$> tags <* R.eof)
368 Left _ -> W.strict_text com
370 tags :: Stream s m Char => ParsecT s u m Doc
374 <*> (W.hcat <$> R.many (R.try (tag_sep >>= (\s -> (s <>) <$> tag_))))
376 tag_sep :: Stream s m Char => ParsecT s u m Doc
378 s <- R.char read_tag_sep
379 sh <- R.many R.space_horizontal
382 <> do W.text $ TL.pack sh
383 tag_ :: Stream s m Char => ParsecT s u m Doc
387 foldMap (\s -> W.dullyellow (W.strict_text s) <> do
388 W.bold $ W.dullblack $ W.char read_tag_value_sep) p <>
389 (W.red $ W.strict_text v)
391 write_comments :: Doc -> [Comment] -> Doc
392 write_comments prefix =
394 List.intersperse W.line .
395 List.map (\c -> prefix <> write_comment c)
399 write_tag :: Tag -> Doc
401 foldMap (\s -> W.dullyellow (W.strict_text s) <> W.char read_tag_value_sep) p <>
402 (W.dullred $ W.strict_text v)
406 write_posting :: Amount_Styles -> Posting_Lengths -> Posting -> Doc
407 write_posting styles max_posting_length
416 let type_ = posting_type p
418 write_status posting_status <> do
419 case Map.null posting_amounts of
420 True -> write_account type_ posting_account
422 let len_acct = write_account_length type_ posting_account in
423 let len_amts = write_amounts_length styles posting_amounts in
424 write_account type_ posting_account <> do
425 W.fill (2 + max_posting_length - (len_acct + len_amts)) (W.space <> W.space) <> do
426 write_amounts styles posting_amounts
427 <> (case posting_comments of
429 [c] -> W.space <> write_comment c
430 _ -> W.line <> do write_comments (write_indent <> W.space) posting_comments)
433 write_indent = W.space <> W.space
435 write_status :: Status -> Doc
436 write_status = \x -> case x of
440 -- ** Type 'Posting_Lengths'
442 type Posting_Lengths = (Int)
444 write_postings_lengths
446 -> Map Account [Posting]
449 write_postings_lengths styles ps pl =
453 ( write_account_length (posting_type p) (posting_account p)
454 + write_amounts_length styles (posting_amounts p) )
456 (Data.Functor.Compose.Compose ps)
458 -- * Write 'Transaction'
460 write_transaction :: Amount_Styles -> Transaction -> Doc
461 write_transaction styles t =
462 write_transaction_with_lengths
463 styles (write_transaction_lengths styles t 0) t
465 write_transactions :: Foldable j => Amount_Styles -> j Transaction -> Doc
466 write_transactions styles j = do
467 let transaction_lengths_ =
468 Data.Foldable.foldr (write_transaction_lengths styles) 0 j
471 write_transaction_with_lengths styles transaction_lengths_ t <>
472 (if W.is_empty doc then W.empty else W.line <> doc)
477 write_transaction_with_lengths
479 -> Transaction_Lengths
480 -> Transaction -> Doc
481 write_transaction_with_lengths
486 , transaction_comments_before
487 , transaction_comments_after
488 , transaction_dates=(first_date, dates)
489 , transaction_postings
491 -- , transaction_tags
492 , transaction_wording
494 (case transaction_comments_before of
496 _ -> write_comments W.space transaction_comments_before <> W.line) <> do
499 (W.char read_date_ymd_sep)
500 (write_date <$> (first_date:dates))) <> do
501 (case transaction_status of
502 True -> W.space <> write_status transaction_status
503 False -> W.empty) <> do
504 write_code transaction_code <> do
505 (case transaction_wording of
507 _ -> W.space <> (W.dullmagenta $ W.strict_text transaction_wording)) <> do
509 (case transaction_comments_after of
511 _ -> write_comments W.space transaction_comments_after <> W.line) <> do
513 (W.vsep . fmap (write_posting styles posting_lengths_))
517 write_code :: Code -> Doc
518 write_code = \x -> case x of
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
535 } posting_lengths = do
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 (Account_Tags (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