1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE MagicHash #-}
4 {-# LANGUAGE NamedFieldPuns #-}
5 {-# LANGUAGE OverloadedStrings #-}
6 {-# LANGUAGE ScopedTypeVariables #-}
7 module Hcompta.Format.Ledger.Write where
9 import Control.Applicative ((<$>), (<*))
10 -- import Control.Arrow ((***))
11 import Data.Decimal (DecimalRaw(..))
12 import qualified Data.Char (isSpace)
13 import Data.Fixed (showFixed)
14 import qualified Data.Functor.Compose
15 import qualified Data.Foldable
16 import Data.Foldable (Foldable)
17 import qualified Data.List
18 import qualified Data.List.NonEmpty
19 import qualified Data.Map.Strict as Data.Map
20 import Data.Maybe (fromMaybe)
21 import qualified Data.Text.Lazy as TL
22 import qualified Data.Text as Text
23 import qualified Data.Time.Calendar as Time (toGregorian)
24 import qualified Data.Time.LocalTime as Time (LocalTime(..), TimeOfDay(..), TimeZone(..), timeZoneOffsetString, ZonedTime(..))
25 import qualified Hcompta.Lib.Leijen as W
26 import Hcompta.Lib.Leijen (Doc, (<>))
27 import System.IO (Handle)
28 import qualified Text.Parsec as R hiding (satisfy, char)
29 import Text.Parsec (Stream, ParsecT)
30 import GHC.Exts (Int(..))
31 import GHC.Integer.Logarithms (integerLogBase#)
33 import qualified Hcompta.Model.Account as Account
34 import Hcompta.Model.Account (Account)
35 import qualified Hcompta.Model.Amount as Amount
36 import Hcompta.Model.Amount (Amount)
37 import qualified Hcompta.Model.Amount.Quantity as Quantity
38 import Hcompta.Model.Amount.Quantity (Quantity)
39 import qualified Hcompta.Model.Amount.Style as Amount.Style
40 import qualified Hcompta.Model.Amount.Unit as Unit
41 import Hcompta.Model.Amount.Unit (Unit)
42 import qualified Hcompta.Format.Ledger as Ledger
43 import Hcompta.Format.Ledger
46 , Posting(..), Posting_by_Account, Posting_Type(..)
50 -- import qualified Hcompta.Model.Date as Date
51 import Hcompta.Model.Date (Date)
52 -- import Hcompta.Format.Ledger.Journal as Journal
53 import qualified Hcompta.Format.Ledger.Read as Read
54 import qualified Hcompta.Lib.Parsec as R
55 import qualified Hcompta.Lib.Foldable as Lib.Foldable
58 -- * Printing 'Account'
60 account :: Posting_Type -> Account -> Doc
63 Posting_Type_Regular -> account_
64 Posting_Type_Virtual -> \acct ->
65 W.char Read.posting_type_virtual_begin <> do
67 W.char Read.posting_type_virtual_end
68 Posting_Type_Virtual_Balanced -> \acct ->
69 W.char Read.posting_type_virtual_balanced_begin <> do
71 W.char Read.posting_type_virtual_balanced_end
73 account_ :: Account -> Doc
76 Data.List.NonEmpty.toList $
77 Data.List.NonEmpty.intersperse
78 (W.bold $ W.yellow $ W.char Read.account_name_sep)
79 (Data.List.NonEmpty.map account_name acct)
81 account_name :: Account.Name -> Doc
82 account_name = W.strict_text
84 -- ** Mesuring 'Account'
86 account_length :: Posting_Type -> Account -> Int
87 account_length type_ acct =
89 (\acc -> (1 +) . (acc +) . Text.length)
92 Posting_Type_Regular -> 0
93 Posting_Type_Virtual -> 2
94 Posting_Type_Virtual_Balanced -> 2
96 -- * Printing 'Amount'
98 amount :: Amount -> Doc
100 { Amount.quantity=qty
101 , Amount.style = sty@(Amount.Style.Style
102 { Amount.Style.unit_side
103 , Amount.Style.unit_spaced
108 Just Amount.Style.Side_Left ->
110 <> (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
114 (Just Amount.Style.Side_Right) ->
115 (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
118 (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
123 unit = W.yellow . W.strict_text . Unit.text
125 quantity :: Amount.Style -> Quantity -> Doc
126 quantity Amount.Style.Style
127 { Amount.Style.fractioning
128 , Amount.Style.grouping_integral
129 , Amount.Style.grouping_fractional
130 , Amount.Style.precision
132 let Decimal e n = Quantity.round precision qty
133 let num = Prelude.show $ abs $ n
134 let sign = W.bold $ W.yellow $ W.strict_text (if n < 0 then "-" else "")
135 case e == 0 || precision == 0 of
136 True -> sign <> do W.bold $ W.blue $ (W.strict_text $ Text.pack num)
138 let num_len = length num
141 [ replicate (fromIntegral e + 1 - num_len) '0'
143 , replicate (fromIntegral precision - fromIntegral e) '0'
145 let (int, frac) = Data.List.splitAt (max 1 (num_len - fromIntegral precision)) padded
146 let default_fractioning =
148 del_grouping_sep grouping_integral $
149 del_grouping_sep grouping_fractional $
153 W.text (TL.pack $ maybe id (\g -> reverse . group g . reverse) grouping_integral $ int) <> do
154 (W.yellow $ W.char (fromMaybe default_fractioning fractioning)) <> do
155 W.text (TL.pack $ maybe id group grouping_fractional frac)
157 group :: Amount.Style.Grouping -> [Char] -> [Char]
158 group (Amount.Style.Grouping sep sizes_) =
159 Data.List.concat . reverse .
160 Data.List.map reverse . fst .
162 (flip (\digit -> \x -> case x of
163 ([], sizes) -> ([[digit]], sizes)
164 (digits:groups, []) -> ((digit:digits):groups, [])
165 (digits:groups, curr_sizes@(size:sizes)) ->
166 if length digits < size
167 then ( (digit:digits):groups, curr_sizes)
168 else ([digit]:[sep]:digits:groups, if sizes == [] then curr_sizes else sizes)
171 del_grouping_sep grouping =
173 Just (Amount.Style.Grouping sep _) -> Data.List.delete sep
176 -- ** Mesuring 'Amount'
178 amount_length :: Amount -> Int
179 amount_length Amount.Amount
180 { Amount.quantity = qty
181 , Amount.style = sty@(Amount.Style.Style
182 { Amount.Style.unit_spaced
184 , Amount.unit = unit_
187 + (case unit_spaced of { Just True | unit_ /= "" -> 1; _ -> 0 })
188 + quantity_length sty qty
190 amounts_length :: Amount.By_Unit -> Int
191 amounts_length amts =
192 if Data.Map.null amts
196 (\n -> (3 +) . (+) (amount_length n))
199 quantity_length :: Amount.Style -> Quantity -> Int
200 quantity_length Amount.Style.Style
201 { Amount.Style.grouping_integral
202 , Amount.Style.grouping_fractional
203 , Amount.Style.precision
205 let Decimal e n = Quantity.round precision qty in
206 let sign_len = if n < 0 then 1 else 0 in
207 let fractioning_len = if e > 0 then 1 else 0 in
208 let num_len = if n == 0 then 0 else (1 +) $ I# (integerLogBase# 10 (abs n)) in
209 let pad_left_len = max 0 (fromIntegral e + 1 - num_len) in
210 let pad_right_len = max 0 (fromIntegral precision - fromIntegral e) in
211 let padded_len = pad_left_len + num_len + pad_right_len in
212 let int_len = max 1 (num_len - fromIntegral precision) in
213 let frac_len = max 0 (padded_len - int_len) in
217 + maybe 0 (group int_len) grouping_integral
218 + maybe 0 (group frac_len) grouping_fractional
221 group :: Int -> Amount.Style.Grouping -> Int
222 group num_len (Amount.Style.Grouping _sep sizes_) =
225 else loop 0 num_len sizes_
227 loop :: Int -> Int -> [Int] -> Int
232 let l = len - size in
234 else loop (pad + 1) l sizes
236 let l = len - size in
238 else loop (pad + 1) l sizes
244 (Time.LocalTime day tod)
245 tz@(Time.TimeZone tz_min _ tz_name)) = do
246 let (y, mo, d) = Time.toGregorian day
247 (if y == 0 then W.empty else W.integer y <> sep '/') <> do
249 sep '/' <> int2 d <> do
251 Time.TimeOfDay 0 0 0 -> W.empty
252 Time.TimeOfDay h m s ->
253 W.space <> int2 h <> do
254 sep ':' <> int2 m <> do
258 (if s < 10 then W.char '0' else W.empty) <> do
259 W.strict_text $ Text.pack $ showFixed True s)) <> do
262 _ | tz_name /= "" -> W.space <> do W.strict_text $ Text.pack tz_name
263 _ -> W.space <> do W.strict_text $ Text.pack $ Time.timeZoneOffsetString tz)
266 int2 i = if i < 10 then W.char '0' <> W.int i else W.int i
268 sep = W.bold . W.dullblack . W.char
270 -- * Printing 'Comment'
272 comment :: Comment -> Doc
275 W.char Read.comment_begin
276 <> (case Text.uncons com of
277 Just (c, _) | not $ Data.Char.isSpace c -> W.space
279 <> do W.if_color colorize (W.strict_text com)
284 pre <- R.many $ R.try $ do
285 ns <- R.many $ R.satisfy (\c -> c /= Read.tag_value_sep && not (Data.Char.isSpace c))
286 sh <- R.space_horizontal
288 ((W.text $ TL.pack $ concat pre) <>) <$> tags <* R.eof)
290 Left _ -> W.strict_text com
292 tags :: Stream s m Char => ParsecT s u m Doc
295 xs <- W.hcat <$> R.many (R.try (tag_sep >>= (\s -> (s <>) <$> tag_)))
298 tag_sep :: Stream s m Char => ParsecT s u m Doc
300 s <- R.char Read.tag_sep
301 sh <- R.many R.space_horizontal
303 do W.bold $ W.dullblack $ W.char s
304 <> do W.text $ TL.pack sh
305 tag_ :: Stream s m Char => ParsecT s u m Doc
308 s <- R.char Read.tag_value_sep
311 (W.yellow $ W.strict_text n)
312 <> (W.bold $ W.dullblack $ W.char s)
313 <> (W.red $ W.strict_text v)
315 comments :: Doc -> [Comment] -> Doc
318 Data.List.intersperse W.line .
319 Data.List.map (\c -> prefix <> comment c)
325 (W.dullyellow $ W.strict_text n)
326 <> W.char Read.tag_value_sep
327 <> (W.dullred $ W.strict_text v)
329 -- * Printing 'Posting'
331 posting :: Posting_Lengths -> Posting_Type -> Posting -> Doc
332 posting max_posting_length type_
334 { posting_account=acct
336 , posting_comments=cmts
338 , posting_status=status_
343 case Data.Map.null posting_amounts of
344 True -> account type_ acct
346 let len_acct = account_length type_ acct in
347 let len_amts = amounts_length posting_amounts in
348 account type_ acct <> do
349 W.fill (2 + max_posting_length - (len_acct + len_amts)) (W.space <> W.space) <> do
351 (W.space <> (W.bold $ W.yellow $ W.char Read.amount_sep) <> W.space)
352 amount posting_amounts
355 [c] -> W.space <> comment c
356 _ -> W.line <> do comments (W.text "\t ") cmts)
358 status :: Ledger.Status -> Doc
359 status = \x -> case x of
363 -- ** Mesuring 'Posting'
365 type Posting_Lengths = (Int)
369 -> Posting_by_Account
372 postings_lengths type_ ps pl =
376 ( account_length type_ (posting_account p)
377 + amounts_length (posting_amounts p) )
379 (Data.Functor.Compose.Compose ps)
381 -- * Printing 'Transaction'
383 transaction :: Transaction -> Doc
384 transaction t = transaction_with_lengths (transaction_lengths t 0) t
386 transactions :: Foldable f => f Transaction -> Doc
388 let transaction_lengths_ =
389 Data.Foldable.foldr transaction_lengths 0 ts
392 transaction_with_lengths transaction_lengths_ t <> W.line <>
393 (if W.is_empty doc then W.empty else W.line <> doc)
398 transaction_with_lengths :: Transaction_Lengths -> Transaction -> Doc
399 transaction_with_lengths
402 { transaction_code=code_
403 , transaction_comments_before
404 , transaction_comments_after
405 , transaction_dates=(first_date, dates)
406 , transaction_description
407 , transaction_postings
408 , transaction_virtual_postings
409 , transaction_balanced_virtual_postings
410 , transaction_status=status_
411 -- , transaction_tags
413 (case transaction_comments_before of
415 _ -> comments W.space transaction_comments_before <> W.line) <> do
417 Data.List.intersperse
418 (W.char Read.date_sep)
419 (Data.List.map date (first_date:dates))) <> do
421 True -> W.space <> status status_
422 False -> W.empty) <> do
424 (case transaction_description of
426 _ -> W.space <> (W.dullmagenta $ W.strict_text transaction_description)) <> do
428 (case transaction_comments_after of
430 _ -> comments W.space transaction_comments_after <> W.line) <> do
431 W.vsep $ Data.List.map
434 (W.intercalate W.line
435 (W.vsep . Data.List.map
436 (posting posting_lengths_ type_)))
437 (Ledger.posting_by_Signs_and_Account ps))
438 [ (Posting_Type_Regular, transaction_postings)
439 , (Posting_Type_Virtual, transaction_virtual_postings)
440 , (Posting_Type_Virtual_Balanced, transaction_balanced_virtual_postings)
443 code :: Ledger.Code -> Doc
444 code = \x -> case x of
446 t -> W.space <> W.char '(' <> W.strict_text t <> W.char ')'
448 -- ** Mesuring 'Transaction'
450 type Transaction_Lengths = Posting_Lengths
452 transaction_lengths :: Transaction -> Posting_Lengths -> Posting_Lengths
455 { transaction_postings
456 , transaction_virtual_postings
457 , transaction_balanced_virtual_postings
458 } posting_lengths_ = do
460 (flip (\(type_, ps) -> postings_lengths type_ ps))
462 [ (Posting_Type_Regular, transaction_postings)
463 , (Posting_Type_Virtual, transaction_virtual_postings)
464 , (Posting_Type_Virtual_Balanced, transaction_balanced_virtual_postings)
467 -- * Printing 'Journal'
469 journal :: Journal -> Doc
470 journal Journal { journal_transactions } =
471 transactions (Data.Functor.Compose.Compose journal_transactions)
477 { style_align :: Bool
478 , style_color :: Bool
487 show :: Style -> Doc -> TL.Text
488 show Style{style_color, style_align} =
491 then W.renderPretty style_color 1.0 maxBound
492 else W.renderCompact style_color
494 put :: Style -> Handle -> Doc -> IO ()
495 put Style{style_color, style_align} handle =
498 then W.renderPretty style_color 1.0 maxBound
499 else W.renderCompact style_color