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 Data.Text (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.Format.Text as W
22 import Hcompta.Format.Text (Doc, (<>))
23 import System.IO (Handle)
24 import qualified Text.Parsec as R
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.Model.Transaction as Transaction
38 import Hcompta.Model.Transaction (Comment, Tag, Transaction)
39 import qualified Hcompta.Model.Transaction.Posting as Posting
40 import Hcompta.Model.Transaction (Posting)
41 import qualified Hcompta.Model.Journal as Journal
42 import Hcompta.Model.Journal (Journal)
43 -- import qualified Hcompta.Model.Transaction.Tag as Tag
44 -- import Hcompta.Model.Transaction (Tag)
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
54 show :: Bool -> Doc -> TL.Text
55 show with_color = W.displayT . W.renderPretty with_color 1.0 maxBound
57 showIO :: Bool -> Handle -> Doc -> IO ()
58 showIO with_color handle = W.displayIO handle . W.renderPretty with_color 1.0 maxBound
62 -- | Return a 'Doc' from a strict 'Text'
64 text = W.text . TL.fromStrict
66 -- | Return a 'Doc' concatenating converted values of a 'Map'
67 -- separated by a given 'Doc'
70 -> Data.Map.Map k a -> Doc
73 (\doc x -> doc <> (if W.is_empty doc then W.empty else sep) <> f x)
76 -- * Printing 'Account'
78 account :: Posting.Type -> Account -> Doc
81 Posting.Type_Regular -> account_
82 Posting.Type_Virtual -> \acct ->
83 W.char Read.posting_type_virtual_begin <> do
85 W.char Read.posting_type_virtual_end
86 Posting.Type_Virtual_Balanced -> \acct ->
87 W.char Read.posting_type_virtual_balanced_begin <> do
89 W.char Read.posting_type_virtual_balanced_end
91 account_ :: Account -> Doc
95 (W.bold $ W.dullblack $ W.char Read.account_name_sep)
96 (Data.List.map account_name acct)
98 account_name :: Account.Name -> Doc
101 -- ** Mesuring 'Account'
103 account_length :: Posting.Type -> Account -> Int
104 account_length type_ acct =
106 (\acc -> (1 +) . (acc +) . Text.length)
107 (if acct == [] then 0 else (- 1)) acct +
109 Posting.Type_Regular -> 0
110 Posting.Type_Virtual -> 2
111 Posting.Type_Virtual_Balanced -> 2
113 -- * Printing 'Amount'
115 amount :: Amount -> Doc
117 { Amount.quantity=qty
118 , Amount.style = style@(Style.Style
125 Just Style.Side_Left ->
127 <> (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
129 <> quantity style qty
131 (Just Style.Side_Right) ->
132 (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
135 (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
140 unit = W.yellow . text
142 quantity :: Style -> Quantity -> Doc
145 , Style.grouping_integral
146 , Style.grouping_fractional
149 let Decimal e n = Quantity.round precision qty
150 let num = Prelude.show $ abs $ n
151 let sign = W.bold $ W.yellow $ text (if n < 0 then "-" else "")
152 case e == 0 || precision == 0 of
153 True -> sign <> do W.bold $ W.blue $ (text $ Text.pack num)
155 let num_len = length num
158 [ replicate (fromIntegral e + 1 - num_len) '0'
160 , replicate (fromIntegral precision - fromIntegral e) '0'
162 let (int, frac) = Data.List.splitAt (max 1 (num_len - fromIntegral precision)) padded
163 let default_fractioning =
165 del_grouping_sep grouping_integral $
166 del_grouping_sep grouping_fractional $
170 W.text (TL.pack $ maybe id (\g -> reverse . group g . reverse) grouping_integral $ int) <> do
171 W.char (fromMaybe default_fractioning fractioning) <> do
172 W.text (TL.pack $ maybe id group grouping_fractional frac)
174 group :: Style.Grouping -> [Char] -> [Char]
175 group (Style.Grouping sep sizes_) =
176 Data.List.concat . reverse .
177 Data.List.map reverse . fst .
179 (flip (\digit -> \case
180 ([], sizes) -> ([[digit]], sizes)
181 (digits:groups, []) -> ((digit:digits):groups, [])
182 (digits:groups, curr_sizes@(size:sizes)) ->
183 if length digits < size
184 then ( (digit:digits):groups, curr_sizes)
185 else ([digit]:[sep]:digits:groups, if sizes == [] then curr_sizes else sizes)
188 del_grouping_sep grouping =
190 Just (Style.Grouping sep _) -> Data.List.delete sep
193 -- ** Mesuring 'Amount'
195 amount_length :: Amount -> Int
196 amount_length Amount.Amount
197 { Amount.quantity=qty
198 , Amount.style = style@(Style.Style
204 + (case unit_spaced of { Just True | unit_ /= "" -> 1; _ -> 0 })
205 + quantity_length style qty
207 amounts_length :: Amount.By_Unit -> Int
208 amounts_length amts =
209 if Data.Map.null amts
213 (\n -> (3 +) . (+) (amount_length n))
216 quantity_length :: Style -> Quantity -> Int
217 quantity_length Style.Style
218 { Style.grouping_integral
219 , Style.grouping_fractional
222 let Decimal e n = Quantity.round precision qty in
223 let sign_len = if n < 0 then 1 else 0 in
224 let fractioning_len = if e > 0 then 1 else 0 in
225 let num_len = if n == 0 then 0 else (1 +) $ truncate $ logBase 10 $ (fromIntegral (abs n)::Double) in
226 let pad_left_len = max 0 (fromIntegral e + 1 - num_len) in
227 let pad_right_len = max 0 (fromIntegral precision - fromIntegral e) in
228 let padded_len = pad_left_len + num_len + pad_right_len in
229 let int_len = max 1 (num_len - fromIntegral precision) in
230 let frac_len = max 0 (padded_len - int_len) in
234 + maybe 0 (group int_len) grouping_integral
235 + maybe 0 (group frac_len) grouping_fractional
238 group :: Int -> Style.Grouping -> Int
239 group num_len (Style.Grouping _sep sizes_) =
242 else loop 0 num_len sizes_
244 loop :: Int -> Int -> [Int] -> Int
249 let l = len - size in
251 else loop (pad + 1) l sizes
253 let l = len - size in
255 else loop (pad + 1) l sizes
261 (Time.LocalTime day tod)
262 tz@(Time.TimeZone tz_min _ tz_name)) = do
263 let (y, mo, d) = Time.toGregorian day
264 (if y == 0 then W.empty else W.integer y <> sep '/') <> do
266 sep '/' <> int2 d <> do
268 Time.TimeOfDay 0 0 0 -> W.empty
269 Time.TimeOfDay h m s ->
270 W.space <> int2 h <> do
271 sep ':' <> int2 m <> do
275 (if s < 10 then W.char '0' else W.empty) <> do
276 text $ Text.pack $ showFixed True s)) <> do
279 _ | tz_name /= "" -> W.space <> do text $ Text.pack tz_name
280 _ -> W.space <> do text $ Text.pack $ Time.timeZoneOffsetString tz)
283 int2 i = if i < 10 then W.char '0' <> W.int i else W.int i
285 sep = W.bold . W.dullblack . W.char
287 -- * Printing 'Comment'
289 comment :: Comment -> Doc
292 W.char Read.comment_begin
293 <> (case Text.uncons com of
294 Just (c, _) | not $ Data.Char.isSpace c -> W.space
296 <> do W.if_color colorize (text com)
301 pre <- R.many $ R.try $ do
302 ns <- R.many $ R.satisfy (\c -> c /= Read.tag_value_sep && not (Data.Char.isSpace c))
303 sh <- Read.space_horizontal
305 ((W.text $ TL.pack $ concat pre) <>) <$> tags <* R.eof)
309 tags :: Stream s m Char => ParsecT s u m Doc
312 xs <- W.hcat <$> R.many (R.try (tag_sep >>= (\s -> (s <>) <$> tag_)))
315 tag_sep :: Stream s m Char => ParsecT s u m Doc
317 s <- R.char Read.tag_sep
318 sh <- R.many Read.space_horizontal
320 do W.bold $ W.dullblack $ W.char s
321 <> do W.text $ TL.pack sh
322 tag_ :: Stream s m Char => ParsecT s u m Doc
325 s <- R.char Read.tag_value_sep
329 <> (W.bold $ W.dullblack $ W.char s)
332 comments :: Doc -> [Comment] -> Doc
335 Data.List.intersperse W.line .
336 Data.List.map (\c -> prefix <> comment c)
342 (W.dullyellow $ text n)
343 <> W.char Read.tag_value_sep
344 <> (W.dullred $ text v)
346 -- * Printing 'Posting'
348 posting :: Posting_Lengths -> Posting.Type -> Posting -> Doc
355 { Posting.account=acct
357 , Posting.comments=cmts
359 , Posting.status=status_
364 case Data.Map.null amounts of
365 True -> account type_ acct
367 W.fill (max_account_length + 2)
368 (account type_ acct) <> do
370 W.displayT $ W.renderPretty False 1.0 maxBound $
372 (W.space <> (W.bold $ W.yellow $ W.char Read.amount_sep) <> W.space)
374 W.fill (max 0 (max_amount_length - (fromIntegral $ TL.length amounts_))) W.empty <> do
378 [c] -> W.space <> comment c
379 _ -> W.line <> do comments (W.text "\t\t") cmts)
381 status :: Transaction.Status -> Doc
386 -- ** Mesuring 'Posting'
388 type Posting_Lengths = (Int, Int)
390 nil_Posting_Lengths :: Posting_Lengths
391 nil_Posting_Lengths = (0, 0)
393 postings_lengths :: Posting.Type -> Posting.By_Account -> Posting_Lengths -> Posting_Lengths
394 postings_lengths type_ =
395 flip $ Data.Map.foldl $ Data.List.foldl $
397 (max (account_length type_ (Posting.account p)))
399 (max (amounts_length (Posting.amounts p)))
401 -- * Printing 'Transaction'
403 transaction :: Transaction -> Doc
404 transaction t = transaction_with_lengths (transaction_lengths t nil_Transaction_Lengths) t
406 transaction_with_lengths :: Transaction_Lengths -> Transaction -> Doc
407 transaction_with_lengths
409 Transaction.Transaction
410 { Transaction.code=code_
411 , Transaction.comments_before
412 , Transaction.comments_after
413 , Transaction.dates=(first_date, dates)
414 , Transaction.description
415 , Transaction.postings
416 , Transaction.virtual_postings
417 , Transaction.balanced_virtual_postings
418 , Transaction.status=status_
419 -- , Transaction.tags
421 (case comments_before of
423 _ -> comments (W.text "\t") comments_before <> W.line) <> do
425 Data.List.intersperse
426 (W.char Read.date_sep)
427 (Data.List.map date (first_date:dates))) <> do
429 True -> W.space <> status status_
430 False -> W.empty) <> do
434 _ -> W.space <> (W.dullmagenta $ text description)) <> do
436 (case comments_after of
438 _ -> comments (W.text "\t") comments_after <> W.line) <> do
439 W.vsep $ Data.List.map
443 (W.vsep . Data.List.map
444 (posting posting_lengths_ type_)))
445 (Posting.by_signs_and_account ps))
446 [ (Posting.Type_Regular, postings)
447 , (Posting.Type_Virtual, virtual_postings)
448 , (Posting.Type_Virtual_Balanced, balanced_virtual_postings)
451 code :: Transaction.Code -> Doc
454 t -> W.space <> W.char '(' <> text t <> W.char ')'
456 -- ** Mesuring 'Transaction'
458 type Transaction_Lengths = Posting_Lengths
460 nil_Transaction_Lengths :: Posting_Lengths
461 nil_Transaction_Lengths = nil_Posting_Lengths
463 transaction_lengths :: Transaction -> Posting_Lengths -> Posting_Lengths
465 Transaction.Transaction
466 { Transaction.postings
467 , Transaction.virtual_postings
468 , Transaction.balanced_virtual_postings
469 } posting_lengths_ = do
471 (flip (\(type_, ps) -> postings_lengths type_ ps))
473 [ (Posting.Type_Regular, postings)
474 , (Posting.Type_Virtual, virtual_postings)
475 , (Posting.Type_Virtual_Balanced, balanced_virtual_postings)
478 -- * Printing 'Journal'
480 journal :: Journal -> Doc
481 journal Journal.Journal
482 { Journal.transactions
484 let transaction_lengths_ =
486 (Data.List.foldl (flip transaction_lengths))
487 nil_Transaction_Lengths
490 (Data.List.foldl (\doc t ->
491 (if W.is_empty doc then W.empty else doc <> W.line)
492 <> transaction_with_lengths transaction_lengths_ t <> W.line