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 qualified Hcompta.Format.Ledger.Read as 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.read_posting_type_virtual_begin <> do
121 W.char Read.read_posting_type_virtual_end
122 Posting_Type_Virtual_Balanced -> \acct ->
123 W.char Read.read_posting_type_virtual_balanced_begin <> do
125 W.char Read.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.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.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.read_comment_begin
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.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.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
385 (p, v) <- Read.read_tag
387 foldMap (\s -> W.dullyellow (W.strict_text s) <> do
388 W.bold $ W.dullblack $ W.char Read.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.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 (W.text "\t ") posting_comments)
432 write_status :: Status -> Doc
433 write_status = \x -> case x of
437 -- ** Type 'Posting_Lengths'
439 type Posting_Lengths = (Int)
441 write_postings_lengths
443 -> Map Account [Posting]
446 write_postings_lengths styles ps pl =
450 ( write_account_length (posting_type p) (posting_account p)
451 + write_amounts_length styles (posting_amounts p) )
453 (Data.Functor.Compose.Compose ps)
455 -- * Write 'Transaction'
457 write_transaction :: Amount_Styles -> Transaction -> Doc
458 write_transaction styles t =
459 write_transaction_with_lengths
460 styles (write_transaction_lengths styles t 0) t
462 write_transactions :: Foldable j => Amount_Styles -> j Transaction -> Doc
463 write_transactions styles j = do
464 let transaction_lengths_ =
465 Data.Foldable.foldr (write_transaction_lengths styles) 0 j
468 write_transaction_with_lengths styles transaction_lengths_ t <>
469 (if W.is_empty doc then W.empty else W.line <> doc)
474 write_transaction_with_lengths
476 -> Transaction_Lengths
477 -> Transaction -> Doc
478 write_transaction_with_lengths
483 , transaction_comments_before
484 , transaction_comments_after
485 , transaction_dates=(first_date, dates)
486 , transaction_postings
488 -- , transaction_tags
489 , transaction_wording
491 (case transaction_comments_before of
493 _ -> write_comments W.space transaction_comments_before <> W.line) <> do
496 (W.char Read.read_date_sep)
497 (write_date <$> (first_date:dates))) <> do
498 (case transaction_status of
499 True -> W.space <> write_status transaction_status
500 False -> W.empty) <> do
501 write_code transaction_code <> do
502 (case transaction_wording of
504 _ -> W.space <> (W.dullmagenta $ W.strict_text transaction_wording)) <> do
506 (case transaction_comments_after of
508 _ -> write_comments W.space transaction_comments_after <> W.line) <> do
510 (W.vsep . fmap (write_posting styles posting_lengths_))
514 write_code :: Code -> Doc
515 write_code = \x -> case x of
517 t -> W.space <> W.char '(' <> W.strict_text t <> W.char ')'
519 -- ** Type 'Transaction_Lengths'
521 type Transaction_Lengths = Posting_Lengths
523 write_transaction_lengths
528 write_transaction_lengths
531 { transaction_postings
532 } posting_lengths = do
534 (flip $ write_postings_lengths styles)
536 [ transaction_postings ]
542 , Monoid (j Transaction)
543 ) => Journal (j Transaction) -> Doc
544 write_journal Journal
545 { journal_amount_styles
547 } = write_transactions journal_amount_styles journal_content
551 write_chart :: Chart -> Doc
553 TreeMap.foldl_with_Path
554 (\doc acct (Account_Tags (Tags ca)) ->
556 write_account Posting_Type_Regular acct <> W.line <>
562 ddd <> W.char '\t' <> write_tag (tn, tv) <> W.line)
571 -- * Type 'Write_Style'
575 { write_style_align :: Bool
576 , write_style_color :: Bool
578 write_style :: Write_Style
581 { write_style_align = True
582 , write_style_color = True
586 write :: Write_Style -> Doc -> TL.Text
589 , write_style_align } =
592 then W.renderPretty write_style_color 1.0 maxBound
593 else W.renderCompact write_style_color
595 writeIO :: Write_Style -> Doc -> Handle -> IO ()
602 then W.renderPretty write_style_color 1.0 maxBound doc
603 else W.renderCompact write_style_color doc