1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE NamedFieldPuns #-}
3 {-# LANGUAGE OverloadedStrings #-}
4 {-# LANGUAGE ScopedTypeVariables #-}
5 module Hcompta.Format.Ledger.Write where
7 import Control.Applicative ((<$>), (<*))
8 import Control.Arrow ((***))
9 import Data.Decimal (DecimalRaw(..))
10 import qualified Data.Char (isSpace)
11 import Data.Fixed (showFixed)
12 import qualified Data.Foldable
13 import qualified Data.List
14 import qualified Data.List.NonEmpty
15 import qualified Data.Map.Strict as Data.Map
16 import Data.Maybe (fromMaybe)
17 import qualified Data.Text.Lazy as TL
18 import qualified Data.Text as Text
19 import qualified Data.Time.Calendar as Time (toGregorian)
20 import qualified Data.Time.LocalTime as Time (LocalTime(..), TimeOfDay(..), TimeZone(..), timeZoneOffsetString, ZonedTime(..))
21 import qualified Hcompta.Lib.Leijen as W
22 import Hcompta.Lib.Leijen (Doc, (<>))
23 import System.IO (Handle)
24 import qualified Text.Parsec as R hiding (satisfy, char)
25 import Text.Parsec (Stream, ParsecT)
27 import qualified Hcompta.Model.Account as Account
28 import Hcompta.Model.Account (Account)
29 import qualified Hcompta.Model.Amount as Amount
30 import Hcompta.Model.Amount (Amount)
31 import qualified Hcompta.Model.Amount.Quantity as Quantity
32 import Hcompta.Model.Amount.Quantity (Quantity)
33 import qualified Hcompta.Model.Amount.Style as Style
34 import Hcompta.Model.Amount.Style (Style)
35 import qualified Hcompta.Model.Amount.Unit as Unit
36 import Hcompta.Model.Amount.Unit (Unit)
37 import qualified Hcompta.Format.Ledger as Ledger
38 import Hcompta.Format.Ledger
41 , Posting(..), Posting_by_Account, Posting_Type(..)
45 -- import qualified Hcompta.Model.Date as Date
46 import Hcompta.Model.Date (Date)
47 -- import Hcompta.Format.Ledger.Journal as Journal
48 import qualified Hcompta.Format.Ledger.Read as Read
49 import qualified Hcompta.Lib.Parsec as R
52 -- * Printing 'Account'
54 account :: Posting_Type -> Account -> Doc
57 Posting_Type_Regular -> account_
58 Posting_Type_Virtual -> \acct ->
59 W.char Read.posting_type_virtual_begin <> do
61 W.char Read.posting_type_virtual_end
62 Posting_Type_Virtual_Balanced -> \acct ->
63 W.char Read.posting_type_virtual_balanced_begin <> do
65 W.char Read.posting_type_virtual_balanced_end
67 account_ :: Account -> Doc
70 Data.List.NonEmpty.toList $
71 Data.List.NonEmpty.intersperse
72 (W.bold $ W.yellow $ W.char Read.account_name_sep)
73 (Data.List.NonEmpty.map account_name acct)
75 account_name :: Account.Name -> Doc
76 account_name = W.strict_text
78 -- ** Mesuring 'Account'
80 account_length :: Posting_Type -> Account -> Int
81 account_length type_ acct =
83 (\acc -> (1 +) . (acc +) . Text.length)
86 Posting_Type_Regular -> 0
87 Posting_Type_Virtual -> 2
88 Posting_Type_Virtual_Balanced -> 2
90 -- * Printing 'Amount'
92 amount :: Amount -> Doc
95 , Amount.style = style@(Style.Style
102 Just Style.Side_Left ->
104 <> (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
106 <> quantity style qty
108 (Just Style.Side_Right) ->
109 (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
112 (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
117 unit = W.yellow . W.strict_text . Unit.text
119 quantity :: Style -> Quantity -> Doc
122 , Style.grouping_integral
123 , Style.grouping_fractional
126 let Decimal e n = Quantity.round precision qty
127 let num = Prelude.show $ abs $ n
128 let sign = W.bold $ W.yellow $ W.strict_text (if n < 0 then "-" else "")
129 case e == 0 || precision == 0 of
130 True -> sign <> do W.bold $ W.blue $ (W.strict_text $ Text.pack num)
132 let num_len = length num
135 [ replicate (fromIntegral e + 1 - num_len) '0'
137 , replicate (fromIntegral precision - fromIntegral e) '0'
139 let (int, frac) = Data.List.splitAt (max 1 (num_len - fromIntegral precision)) padded
140 let default_fractioning =
142 del_grouping_sep grouping_integral $
143 del_grouping_sep grouping_fractional $
147 W.text (TL.pack $ maybe id (\g -> reverse . group g . reverse) grouping_integral $ int) <> do
148 (W.yellow $ W.char (fromMaybe default_fractioning fractioning)) <> do
149 W.text (TL.pack $ maybe id group grouping_fractional frac)
151 group :: Style.Grouping -> [Char] -> [Char]
152 group (Style.Grouping sep sizes_) =
153 Data.List.concat . reverse .
154 Data.List.map reverse . fst .
156 (flip (\digit -> \x -> case x of
157 ([], sizes) -> ([[digit]], sizes)
158 (digits:groups, []) -> ((digit:digits):groups, [])
159 (digits:groups, curr_sizes@(size:sizes)) ->
160 if length digits < size
161 then ( (digit:digits):groups, curr_sizes)
162 else ([digit]:[sep]:digits:groups, if sizes == [] then curr_sizes else sizes)
165 del_grouping_sep grouping =
167 Just (Style.Grouping sep _) -> Data.List.delete sep
170 -- ** Mesuring 'Amount'
172 amount_length :: Amount -> Int
173 amount_length Amount.Amount
174 { Amount.quantity=qty
175 , Amount.style = style@(Style.Style
181 + (case unit_spaced of { Just True | unit_ /= "" -> 1; _ -> 0 })
182 + quantity_length style qty
184 amounts_length :: Amount.By_Unit -> Int
185 amounts_length amts =
186 if Data.Map.null amts
190 (\n -> (3 +) . (+) (amount_length n))
193 quantity_length :: Style -> Quantity -> Int
194 quantity_length Style.Style
195 { Style.grouping_integral
196 , Style.grouping_fractional
199 let Decimal e n = Quantity.round precision qty in
200 let sign_len = if n < 0 then 1 else 0 in
201 let fractioning_len = if e > 0 then 1 else 0 in
202 let num_len = if n == 0 then 0 else (1 +) $ truncate $ logBase 10 $ (fromIntegral (abs n)::Double) in
203 let pad_left_len = max 0 (fromIntegral e + 1 - num_len) in
204 let pad_right_len = max 0 (fromIntegral precision - fromIntegral e) in
205 let padded_len = pad_left_len + num_len + pad_right_len in
206 let int_len = max 1 (num_len - fromIntegral precision) in
207 let frac_len = max 0 (padded_len - int_len) in
211 + maybe 0 (group int_len) grouping_integral
212 + maybe 0 (group frac_len) grouping_fractional
215 group :: Int -> Style.Grouping -> Int
216 group num_len (Style.Grouping _sep sizes_) =
219 else loop 0 num_len sizes_
221 loop :: Int -> Int -> [Int] -> Int
226 let l = len - size in
228 else loop (pad + 1) l sizes
230 let l = len - size in
232 else loop (pad + 1) l sizes
238 (Time.LocalTime day tod)
239 tz@(Time.TimeZone tz_min _ tz_name)) = do
240 let (y, mo, d) = Time.toGregorian day
241 (if y == 0 then W.empty else W.integer y <> sep '/') <> do
243 sep '/' <> int2 d <> do
245 Time.TimeOfDay 0 0 0 -> W.empty
246 Time.TimeOfDay h m s ->
247 W.space <> int2 h <> do
248 sep ':' <> int2 m <> do
252 (if s < 10 then W.char '0' else W.empty) <> do
253 W.strict_text $ Text.pack $ showFixed True s)) <> do
256 _ | tz_name /= "" -> W.space <> do W.strict_text $ Text.pack tz_name
257 _ -> W.space <> do W.strict_text $ Text.pack $ Time.timeZoneOffsetString tz)
260 int2 i = if i < 10 then W.char '0' <> W.int i else W.int i
262 sep = W.bold . W.dullblack . W.char
264 -- * Printing 'Comment'
266 comment :: Comment -> Doc
269 W.char Read.comment_begin
270 <> (case Text.uncons com of
271 Just (c, _) | not $ Data.Char.isSpace c -> W.space
273 <> do W.if_color colorize (W.strict_text com)
278 pre <- R.many $ R.try $ do
279 ns <- R.many $ R.satisfy (\c -> c /= Read.tag_value_sep && not (Data.Char.isSpace c))
280 sh <- R.space_horizontal
282 ((W.text $ TL.pack $ concat pre) <>) <$> tags <* R.eof)
284 Left _ -> W.strict_text com
286 tags :: Stream s m Char => ParsecT s u m Doc
289 xs <- W.hcat <$> R.many (R.try (tag_sep >>= (\s -> (s <>) <$> tag_)))
292 tag_sep :: Stream s m Char => ParsecT s u m Doc
294 s <- R.char Read.tag_sep
295 sh <- R.many R.space_horizontal
297 do W.bold $ W.dullblack $ W.char s
298 <> do W.text $ TL.pack sh
299 tag_ :: Stream s m Char => ParsecT s u m Doc
302 s <- R.char Read.tag_value_sep
305 (W.yellow $ W.strict_text n)
306 <> (W.bold $ W.dullblack $ W.char s)
307 <> (W.red $ W.strict_text v)
309 comments :: Doc -> [Comment] -> Doc
312 Data.List.intersperse W.line .
313 Data.List.map (\c -> prefix <> comment c)
319 (W.dullyellow $ W.strict_text n)
320 <> W.char Read.tag_value_sep
321 <> (W.dullred $ W.strict_text v)
323 -- * Printing 'Posting'
325 posting :: Posting_Lengths -> Posting_Type -> Posting -> Doc
332 { posting_account=acct
334 , posting_comments=cmts
336 , posting_status=status_
341 case Data.Map.null posting_amounts of
342 True -> account type_ acct
344 W.fill (max_account_length + 2)
345 (account type_ acct) <> do
348 - (fromIntegral $ amounts_length posting_amounts) )) W.empty <> do
350 (W.space <> (W.bold $ W.yellow $ W.char Read.amount_sep) <> W.space)
351 amount posting_amounts
354 [c] -> W.space <> comment c
355 _ -> W.line <> do comments (W.text "\t ") cmts)
357 status :: Ledger.Status -> Doc
358 status = \x -> case x of
362 -- ** Mesuring 'Posting'
364 type Posting_Lengths = (Int, Int)
366 nil_Posting_Lengths :: Posting_Lengths
367 nil_Posting_Lengths = (0, 0)
369 postings_lengths :: Posting_Type -> Posting_by_Account -> Posting_Lengths -> Posting_Lengths
370 postings_lengths type_ =
371 flip $ Data.Map.foldl $ Data.List.foldl $
373 (max (account_length type_ (posting_account p)))
375 (max (amounts_length (posting_amounts p)))
377 -- * Printing 'Transaction'
379 transaction :: Transaction -> Doc
380 transaction t = transaction_with_lengths (transaction_lengths t nil_Transaction_Lengths) t
382 transaction_with_lengths :: Transaction_Lengths -> Transaction -> Doc
383 transaction_with_lengths
386 { transaction_code=code_
387 , transaction_comments_before
388 , transaction_comments_after
389 , transaction_dates=(first_date, dates)
390 , transaction_description
391 , transaction_postings
392 , transaction_virtual_postings
393 , transaction_balanced_virtual_postings
394 , transaction_status=status_
395 -- , transaction_tags
397 (case transaction_comments_before of
399 _ -> comments W.space transaction_comments_before <> W.line) <> do
401 Data.List.intersperse
402 (W.char Read.date_sep)
403 (Data.List.map date (first_date:dates))) <> do
405 True -> W.space <> status status_
406 False -> W.empty) <> do
408 (case transaction_description of
410 _ -> W.space <> (W.dullmagenta $ W.strict_text transaction_description)) <> do
412 (case transaction_comments_after of
414 _ -> comments W.space transaction_comments_after <> W.line) <> do
415 W.vsep $ Data.List.map
418 (W.intercalate W.line
419 (W.vsep . Data.List.map
420 (posting posting_lengths_ type_)))
421 (Ledger.posting_by_Signs_and_Account ps))
422 [ (Posting_Type_Regular, transaction_postings)
423 , (Posting_Type_Virtual, transaction_virtual_postings)
424 , (Posting_Type_Virtual_Balanced, transaction_balanced_virtual_postings)
427 code :: Ledger.Code -> Doc
428 code = \x -> case x of
430 t -> W.space <> W.char '(' <> W.strict_text t <> W.char ')'
432 -- ** Mesuring 'Transaction'
434 type Transaction_Lengths = Posting_Lengths
436 nil_Transaction_Lengths :: Posting_Lengths
437 nil_Transaction_Lengths = nil_Posting_Lengths
439 transaction_lengths :: Transaction -> Posting_Lengths -> Posting_Lengths
442 { transaction_postings
443 , transaction_virtual_postings
444 , transaction_balanced_virtual_postings
445 } posting_lengths_ = do
447 (flip (\(type_, ps) -> postings_lengths type_ ps))
449 [ (Posting_Type_Regular, transaction_postings)
450 , (Posting_Type_Virtual, transaction_virtual_postings)
451 , (Posting_Type_Virtual_Balanced, transaction_balanced_virtual_postings)
454 -- * Printing 'Journal'
456 journal :: Journal -> Doc
458 { journal_transactions
460 let transaction_lengths_ =
462 (flip (Data.List.foldr transaction_lengths))
463 nil_Transaction_Lengths
466 (Data.List.foldl' (\doc t ->
467 (if W.is_empty doc then W.empty else doc <> W.line)
468 <> transaction_with_lengths transaction_lengths_ t <> W.line
475 show :: Bool -> Doc -> TL.Text
476 show with_color = W.displayT . W.renderPretty with_color 1.0 maxBound
478 put :: Bool -> Handle -> Doc -> IO ()
479 put with_color handle = W.displayIO handle . W.renderPretty with_color 1.0 maxBound