1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE LambdaCase #-}
3 {-# LANGUAGE NamedFieldPuns #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE ScopedTypeVariables #-}
6 module Hcompta.Format.Ledger.Write where
8 import Control.Applicative ((<$>), (<*))
9 import Control.Arrow ((***))
10 import Data.Decimal (DecimalRaw(..))
11 import qualified Data.Char (isSpace)
12 import Data.Fixed (showFixed)
13 import qualified Data.List
14 import qualified Data.Map.Strict as Data.Map
15 import Data.Maybe (fromMaybe)
16 import qualified Data.Text.Lazy as TL
17 import qualified Data.Text as Text
18 import qualified Data.Time.Calendar as Time (toGregorian)
19 import qualified Data.Time.LocalTime as Time (LocalTime(..), TimeOfDay(..), TimeZone(..), timeZoneOffsetString, ZonedTime(..))
20 import qualified Hcompta.Lib.Leijen as W
21 import Hcompta.Lib.Leijen (Doc, (<>))
22 import System.IO (Handle)
23 import qualified Text.Parsec as R
24 import Text.Parsec (Stream, ParsecT)
26 import qualified Hcompta.Model.Account as Account
27 import Hcompta.Model.Account (Account)
28 import qualified Hcompta.Model.Amount as Amount
29 import Hcompta.Model.Amount (Amount)
30 import qualified Hcompta.Model.Amount.Quantity as Quantity
31 import Hcompta.Model.Amount.Quantity (Quantity)
32 import qualified Hcompta.Model.Amount.Style as Style
33 import Hcompta.Model.Amount.Style (Style)
34 -- import qualified Hcompta.Model.Amount.Unit as Unit
35 import Hcompta.Model.Amount.Unit (Unit)
36 import qualified Hcompta.Model.Transaction as Transaction
37 import Hcompta.Model.Transaction (Comment, Tag, Transaction)
38 import qualified Hcompta.Model.Transaction.Posting as Posting
39 import Hcompta.Model.Transaction (Posting)
40 import qualified Hcompta.Model.Journal as Journal
41 import Hcompta.Model.Journal (Journal)
42 -- import qualified Hcompta.Model.Transaction.Tag as Tag
43 -- import Hcompta.Model.Transaction (Tag)
44 -- import qualified Hcompta.Model.Date as Date
45 import Hcompta.Model.Date (Date)
46 -- import Hcompta.Format.Ledger.Journal as Journal
47 import qualified Hcompta.Format.Ledger.Read as Read
48 import qualified Hcompta.Lib.Parsec as R
51 -- * Printing 'Account'
53 account :: Posting.Type -> Account -> Doc
56 Posting.Type_Regular -> account_
57 Posting.Type_Virtual -> \acct ->
58 W.char Read.posting_type_virtual_begin <> do
60 W.char Read.posting_type_virtual_end
61 Posting.Type_Virtual_Balanced -> \acct ->
62 W.char Read.posting_type_virtual_balanced_begin <> do
64 W.char Read.posting_type_virtual_balanced_end
66 account_ :: Account -> Doc
70 (W.bold $ W.dullblack $ W.char Read.account_name_sep)
71 (Data.List.map account_name acct)
73 account_name :: Account.Name -> Doc
74 account_name = W.strict_text
76 -- ** Mesuring 'Account'
78 account_length :: Posting.Type -> Account -> Int
79 account_length type_ acct =
81 (\acc -> (1 +) . (acc +) . Text.length)
82 (if acct == [] then 0 else (- 1)) acct +
84 Posting.Type_Regular -> 0
85 Posting.Type_Virtual -> 2
86 Posting.Type_Virtual_Balanced -> 2
88 -- * Printing 'Amount'
90 amount :: Amount -> Doc
93 , Amount.style = style@(Style.Style
100 Just Style.Side_Left ->
102 <> (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
104 <> quantity style qty
106 (Just Style.Side_Right) ->
107 (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
110 (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
115 unit = W.yellow . W.strict_text
117 quantity :: Style -> Quantity -> Doc
120 , Style.grouping_integral
121 , Style.grouping_fractional
124 let Decimal e n = Quantity.round precision qty
125 let num = Prelude.show $ abs $ n
126 let sign = W.bold $ W.yellow $ W.strict_text (if n < 0 then "-" else "")
127 case e == 0 || precision == 0 of
128 True -> sign <> do W.bold $ W.blue $ (W.strict_text $ Text.pack num)
130 let num_len = length num
133 [ replicate (fromIntegral e + 1 - num_len) '0'
135 , replicate (fromIntegral precision - fromIntegral e) '0'
137 let (int, frac) = Data.List.splitAt (max 1 (num_len - fromIntegral precision)) padded
138 let default_fractioning =
140 del_grouping_sep grouping_integral $
141 del_grouping_sep grouping_fractional $
145 W.text (TL.pack $ maybe id (\g -> reverse . group g . reverse) grouping_integral $ int) <> do
146 W.char (fromMaybe default_fractioning fractioning) <> do
147 W.text (TL.pack $ maybe id group grouping_fractional frac)
149 group :: Style.Grouping -> [Char] -> [Char]
150 group (Style.Grouping sep sizes_) =
151 Data.List.concat . reverse .
152 Data.List.map reverse . fst .
154 (flip (\digit -> \case
155 ([], sizes) -> ([[digit]], sizes)
156 (digits:groups, []) -> ((digit:digits):groups, [])
157 (digits:groups, curr_sizes@(size:sizes)) ->
158 if length digits < size
159 then ( (digit:digits):groups, curr_sizes)
160 else ([digit]:[sep]:digits:groups, if sizes == [] then curr_sizes else sizes)
163 del_grouping_sep grouping =
165 Just (Style.Grouping sep _) -> Data.List.delete sep
168 -- ** Mesuring 'Amount'
170 amount_length :: Amount -> Int
171 amount_length Amount.Amount
172 { Amount.quantity=qty
173 , Amount.style = style@(Style.Style
179 + (case unit_spaced of { Just True | unit_ /= "" -> 1; _ -> 0 })
180 + quantity_length style qty
182 amounts_length :: Amount.By_Unit -> Int
183 amounts_length amts =
184 if Data.Map.null amts
188 (\n -> (3 +) . (+) (amount_length n))
191 quantity_length :: Style -> Quantity -> Int
192 quantity_length Style.Style
193 { Style.grouping_integral
194 , Style.grouping_fractional
197 let Decimal e n = Quantity.round precision qty in
198 let sign_len = if n < 0 then 1 else 0 in
199 let fractioning_len = if e > 0 then 1 else 0 in
200 let num_len = if n == 0 then 0 else (1 +) $ truncate $ logBase 10 $ (fromIntegral (abs n)::Double) in
201 let pad_left_len = max 0 (fromIntegral e + 1 - num_len) in
202 let pad_right_len = max 0 (fromIntegral precision - fromIntegral e) in
203 let padded_len = pad_left_len + num_len + pad_right_len in
204 let int_len = max 1 (num_len - fromIntegral precision) in
205 let frac_len = max 0 (padded_len - int_len) in
209 + maybe 0 (group int_len) grouping_integral
210 + maybe 0 (group frac_len) grouping_fractional
213 group :: Int -> Style.Grouping -> Int
214 group num_len (Style.Grouping _sep sizes_) =
217 else loop 0 num_len sizes_
219 loop :: Int -> Int -> [Int] -> Int
224 let l = len - size in
226 else loop (pad + 1) l sizes
228 let l = len - size in
230 else loop (pad + 1) l sizes
236 (Time.LocalTime day tod)
237 tz@(Time.TimeZone tz_min _ tz_name)) = do
238 let (y, mo, d) = Time.toGregorian day
239 (if y == 0 then W.empty else W.integer y <> sep '/') <> do
241 sep '/' <> int2 d <> do
243 Time.TimeOfDay 0 0 0 -> W.empty
244 Time.TimeOfDay h m s ->
245 W.space <> int2 h <> do
246 sep ':' <> int2 m <> do
250 (if s < 10 then W.char '0' else W.empty) <> do
251 W.strict_text $ Text.pack $ showFixed True s)) <> do
254 _ | tz_name /= "" -> W.space <> do W.strict_text $ Text.pack tz_name
255 _ -> W.space <> do W.strict_text $ Text.pack $ Time.timeZoneOffsetString tz)
258 int2 i = if i < 10 then W.char '0' <> W.int i else W.int i
260 sep = W.bold . W.dullblack . W.char
262 -- * Printing 'Comment'
264 comment :: Comment -> Doc
267 W.char Read.comment_begin
268 <> (case Text.uncons com of
269 Just (c, _) | not $ Data.Char.isSpace c -> W.space
271 <> do W.if_color colorize (W.strict_text com)
276 pre <- R.many $ R.try $ do
277 ns <- R.many $ R.satisfy (\c -> c /= Read.tag_value_sep && not (Data.Char.isSpace c))
278 sh <- R.space_horizontal
280 ((W.text $ TL.pack $ concat pre) <>) <$> tags <* R.eof)
282 Left _ -> W.strict_text com
284 tags :: Stream s m Char => ParsecT s u m Doc
287 xs <- W.hcat <$> R.many (R.try (tag_sep >>= (\s -> (s <>) <$> tag_)))
290 tag_sep :: Stream s m Char => ParsecT s u m Doc
292 s <- R.char Read.tag_sep
293 sh <- R.many R.space_horizontal
295 do W.bold $ W.dullblack $ W.char s
296 <> do W.text $ TL.pack sh
297 tag_ :: Stream s m Char => ParsecT s u m Doc
300 s <- R.char Read.tag_value_sep
303 (W.yellow $ W.strict_text n)
304 <> (W.bold $ W.dullblack $ W.char s)
305 <> (W.red $ W.strict_text v)
307 comments :: Doc -> [Comment] -> Doc
310 Data.List.intersperse W.line .
311 Data.List.map (\c -> prefix <> comment c)
317 (W.dullyellow $ W.strict_text n)
318 <> W.char Read.tag_value_sep
319 <> (W.dullred $ W.strict_text v)
321 -- * Printing 'Posting'
323 posting :: Posting_Lengths -> Posting.Type -> Posting -> Doc
330 { Posting.account=acct
332 , Posting.comments=cmts
334 , Posting.status=status_
339 case Data.Map.null amounts of
340 True -> account type_ acct
342 W.fill (max_account_length + 2)
343 (account type_ acct) <> do
346 - (fromIntegral $ amounts_length amounts) )) W.empty <> do
348 (W.space <> (W.bold $ W.yellow $ W.char Read.amount_sep) <> W.space)
352 [c] -> W.space <> comment c
353 _ -> W.line <> do comments (W.text "\t\t") cmts)
355 status :: Transaction.Status -> Doc
360 -- ** Mesuring 'Posting'
362 type Posting_Lengths = (Int, Int)
364 nil_Posting_Lengths :: Posting_Lengths
365 nil_Posting_Lengths = (0, 0)
367 postings_lengths :: Posting.Type -> Posting.By_Account -> Posting_Lengths -> Posting_Lengths
368 postings_lengths type_ =
369 flip $ Data.Map.foldl $ Data.List.foldl $
371 (max (account_length type_ (Posting.account p)))
373 (max (amounts_length (Posting.amounts p)))
375 -- * Printing 'Transaction'
377 transaction :: Transaction -> Doc
378 transaction t = transaction_with_lengths (transaction_lengths t nil_Transaction_Lengths) t
380 transaction_with_lengths :: Transaction_Lengths -> Transaction -> Doc
381 transaction_with_lengths
383 Transaction.Transaction
384 { Transaction.code=code_
385 , Transaction.comments_before
386 , Transaction.comments_after
387 , Transaction.dates=(first_date, dates)
388 , Transaction.description
389 , Transaction.postings
390 , Transaction.virtual_postings
391 , Transaction.balanced_virtual_postings
392 , Transaction.status=status_
393 -- , Transaction.tags
395 (case comments_before of
397 _ -> comments (W.text "\t") comments_before <> W.line) <> do
399 Data.List.intersperse
400 (W.char Read.date_sep)
401 (Data.List.map date (first_date:dates))) <> do
403 True -> W.space <> status status_
404 False -> W.empty) <> do
408 _ -> W.space <> (W.dullmagenta $ W.strict_text description)) <> do
410 (case comments_after of
412 _ -> comments (W.text "\t") comments_after <> W.line) <> do
413 W.vsep $ Data.List.map
416 (W.intercalate W.line
417 (W.vsep . Data.List.map
418 (posting posting_lengths_ type_)))
419 (Posting.by_signs_and_account ps))
420 [ (Posting.Type_Regular, postings)
421 , (Posting.Type_Virtual, virtual_postings)
422 , (Posting.Type_Virtual_Balanced, balanced_virtual_postings)
425 code :: Transaction.Code -> Doc
428 t -> W.space <> W.char '(' <> W.strict_text t <> W.char ')'
430 -- ** Mesuring 'Transaction'
432 type Transaction_Lengths = Posting_Lengths
434 nil_Transaction_Lengths :: Posting_Lengths
435 nil_Transaction_Lengths = nil_Posting_Lengths
437 transaction_lengths :: Transaction -> Posting_Lengths -> Posting_Lengths
439 Transaction.Transaction
440 { Transaction.postings
441 , Transaction.virtual_postings
442 , Transaction.balanced_virtual_postings
443 } posting_lengths_ = do
445 (flip (\(type_, ps) -> postings_lengths type_ ps))
447 [ (Posting.Type_Regular, postings)
448 , (Posting.Type_Virtual, virtual_postings)
449 , (Posting.Type_Virtual_Balanced, balanced_virtual_postings)
452 -- * Printing 'Journal'
454 journal :: Journal -> Doc
455 journal Journal.Journal
456 { Journal.transactions
458 let transaction_lengths_ =
460 (Data.List.foldl (flip transaction_lengths))
461 nil_Transaction_Lengths
464 (Data.List.foldl (\doc t ->
465 (if W.is_empty doc then W.empty else doc <> W.line)
466 <> transaction_with_lengths transaction_lengths_ t <> W.line
473 show :: Bool -> Doc -> TL.Text
474 show with_color = W.displayT . W.renderPretty with_color 1.0 maxBound
476 put :: Bool -> Handle -> Doc -> IO ()
477 put with_color handle = W.displayIO handle . W.renderPretty with_color 1.0 maxBound