1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE NamedFieldPuns #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE ScopedTypeVariables #-}
6 {-# LANGUAGE StandaloneDeriving #-}
7 {-# LANGUAGE TupleSections #-}
8 {-# LANGUAGE TypeFamilies #-}
9 module Hcompta.Format.Ledger.Read where
11 import Control.Applicative ((<$>), (<*>), (<*))
12 import Control.Arrow ((***), first)
13 import Control.DeepSeq (NFData(..))
14 import qualified Control.Exception as Exception
15 import Control.Monad (Monad(..), guard, liftM, join, forM, void)
16 import Control.Monad.IO.Class (liftIO)
17 import Control.Monad.Trans.Except (ExceptT(..), throwE)
19 import Data.Char (Char, isSpace)
20 import Data.Either (Either(..), either)
21 import Data.Eq (Eq(..))
22 import qualified Data.List as List
23 import Data.List.NonEmpty (NonEmpty(..))
24 import qualified Data.List.NonEmpty as NonEmpty
25 import Data.Map.Strict (Map)
26 import qualified Data.Map.Strict as Map
27 import Data.Maybe (Maybe(..), fromMaybe, maybe)
28 import Data.Monoid (Monoid(..))
29 import Data.String (fromString)
30 import qualified Data.Text as Text
31 import qualified Data.Text.IO as Text.IO (readFile)
32 import qualified Data.Time.Calendar as Time
33 import qualified Data.Time.Clock as Time
34 import qualified Data.Time.LocalTime as Time
35 import Data.Typeable ()
36 import Prelude (($), (.), IO, FilePath, const, flip, id)
37 import qualified System.FilePath.Posix as Path
38 import qualified Text.Parsec as R hiding
51 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
52 import qualified Text.Parsec.Pos as R
53 import Text.Show (Show)
55 -- import qualified Hcompta.Account as Account
56 import qualified Hcompta.Amount as Amount
57 import qualified Hcompta.Balance as Balance
58 import qualified Hcompta.Chart as Chart
59 import Hcompta.Date (Date)
60 import qualified Hcompta.Date as Date
61 import qualified Hcompta.Filter.Date.Read as Date.Read
62 import Hcompta.Format.Ledger
70 import qualified Hcompta.Format.Ledger as Ledger
71 import qualified Hcompta.Format.Ledger.Account.Read as Ledger.Account.Read
72 import qualified Hcompta.Format.Ledger.Amount as Ledger.Amount
73 import qualified Hcompta.Format.Ledger.Amount.Read as Ledger.Amount.Read
74 import qualified Hcompta.Format.Ledger.Amount.Style as Ledger.Amount.Style
75 -- import qualified Hcompta.Format.Ledger.Quantity as Ledger.Quantity
76 import Hcompta.Lib.Consable (Consable(..))
77 import qualified Hcompta.Lib.Parsec as R
78 import qualified Hcompta.Lib.Path as Path
79 import Hcompta.Lib.Regex (Regex)
80 import qualified Hcompta.Lib.TreeMap as TreeMap
81 import qualified Hcompta.Polarize as Polarize
82 import qualified Hcompta.Quantity as Quantity
83 import Hcompta.Tag (Tag)
84 import qualified Hcompta.Tag as Tag
85 import qualified Hcompta.Unit as Unit
91 { context_account_prefix :: !(Maybe Account)
92 , context_aliases_exact :: !(Map Account Account)
93 , context_aliases_joker :: ![(Ledger.Account_Joker, Account)]
94 , context_aliases_regex :: ![(Regex, Account)]
95 , context_date :: !Date
96 , context_filter :: !f
97 , context_journal :: !(Journal (ts (Chart_With t)))
98 , context_unit :: !(Maybe Ledger.Unit)
99 , context_year :: !Date.Year
103 :: (Show f, Consable f ts (Chart_With t))
104 => f -> Journal (ts (Chart_With t)) -> Context f ts t
105 context flt context_journal =
107 { context_account_prefix = Nothing
108 , context_aliases_exact = mempty
109 , context_aliases_joker = []
110 , context_aliases_regex = []
111 , context_date = Date.nil
112 , context_filter = flt
114 , context_unit = Nothing
115 , context_year = Date.year Date.nil
121 = Error_date Date.Read.Error
122 | Error_transaction_not_equilibrated
126 , Balance.Unit_Sum Account
127 (Polarize.Polarized Ledger.Quantity)
129 | Error_virtual_transaction_not_equilibrated
133 , Balance.Unit_Sum Account
134 (Polarize.Polarized Ledger.Quantity)
136 | Error_reading_file FilePath Exception.IOException
137 | Error_including_file FilePath [R.Error Error]
143 :: (Consable f ts (Chart_With t), Stream s m Char)
144 => ParsecT s (Context f ts t) m ()
146 _ <- R.string "alias"
147 R.skipMany1 $ R.space_horizontal
148 pattern <- Ledger.Account.Read.pattern
149 R.skipMany $ R.space_horizontal
151 R.skipMany $ R.space_horizontal
152 repl <- Ledger.Account.Read.account
153 R.skipMany $ R.space_horizontal
155 Ledger.Account_Pattern_Exact acct -> R.modifyState $ \ctx -> ctx{context_aliases_exact=
156 Map.insert acct repl $ context_aliases_exact ctx}
157 Ledger.Account_Pattern_Joker jokr -> R.modifyState $ \ctx -> ctx{context_aliases_joker=
158 (jokr, repl):context_aliases_joker ctx}
159 Ledger.Account_Pattern_Regex regx -> R.modifyState $ \ctx -> ctx{context_aliases_regex=
160 (regx, repl):context_aliases_regex ctx}
165 comment_begin :: Char
168 comment :: Stream s m Char => ParsecT s u m Comment
170 _ <- R.char comment_begin
172 R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
175 comments :: Stream s m Char => ParsecT s u m [Comment]
179 R.many1_separated comment (R.new_line >> R.skipMany R.space_horizontal)
185 tag_value_sep :: Char
191 tag_path_section_char :: Stream s m Char => ParsecT s u m Char
192 tag_path_section_char =
193 R.satisfy (\c -> c /= tag_value_sep && c /= tag_sep && not (Data.Char.isSpace c))
195 tag :: Stream s m Char => ParsecT s u m Tag
196 tag = ((,) <$> tag_path <*> tag_value) <?> "tag"
198 tag_path :: Stream s m Char => ParsecT s u m Tag.Path
200 NonEmpty.fromList <$> do
201 R.many1 $ R.try tag_path_section
203 tag_path_section :: Stream s m Char => ParsecT s u m Tag.Section
204 tag_path_section = do
206 ((R.many1 $ tag_path_section_char) <* R.char tag_value_sep)
208 tag_value :: Stream s m Char => ParsecT s u m Tag.Value
211 R.manyTill R.anyChar $ do
213 R.try (R.char tag_sep >> R.many R.space_horizontal >> void tag_path_section)
214 <|> R.try (void (R.try R.new_line))
217 tags :: Stream s m Char => ParsecT s u m (Map Tag.Path [Tag.Value])
219 Map.fromListWith (flip mappend)
220 . List.map (\(p, v) -> (p, [v])) <$> do
221 R.many_separated tag $ do
223 R.skipMany $ R.space_horizontal
225 not_tag :: Stream s m Char => ParsecT s u m ()
227 R.skipMany $ R.try $ do
228 R.skipMany $ tag_path_section_char
234 ( Consable f ts (Chart_With t)
235 , Stream s (R.Error_State Error m) Char
237 ) => ParsecT s (Context f ts t) (R.Error_State Error m) (Ledger.Posting_Typed Posting)
239 posting_sourcepos <- R.getPosition
240 R.skipMany1 $ R.space_horizontal
241 posting_status <- status
242 R.skipMany $ R.space_horizontal
243 acct <- Ledger.Account.Read.account
244 let Ledger.Posting_Typed type_ posting_account = posting_type acct
248 (void R.tab <|> void (R.count 2 R.space_horizontal))
249 R.skipMany $ R.space_horizontal
251 R.many_separated Ledger.Amount.Read.amount $ do
252 R.skipMany $ R.space_horizontal
253 _ <- R.char amount_sep
254 R.skipMany $ R.space_horizontal
255 ctx <- flip liftM R.getState $ \ctx ->
258 let jnl = context_journal ctx in
260 { Ledger.journal_amount_styles =
262 (\(Ledger.Amount.Style.Styles styles) (style, amt) ->
263 Ledger.Amount.Style.Styles $
264 Map.insertWith mappend
265 (Amount.amount_unit amt)
267 (Ledger.journal_amount_styles jnl)
273 Map.fromListWith Quantity.quantity_add $
276 let unit = Amount.amount_unit amt in
277 ( if unit == Unit.unit_empty
278 then maybe unit id (context_unit ctx)
280 , Amount.amount_quantity amt
286 R.skipMany $ R.space_horizontal
287 -- TODO: balance assertion
289 posting_comments <- comments
290 let posting_tags@(Tag.Tags tags_) = tags_of_comments posting_comments
293 case Map.lookup ("date":|[]) tags_ of
296 let date2s = Map.lookup ("date2":|[]) tags_ -- NOTE: support hledger's date2
298 forM (dates `mappend` fromMaybe [] date2s) $ \s ->
299 R.runParserT_with_Error_fail "tag date" id
300 (Date.Read.date Error_date (Just $ context_year ctx) <* R.eof) ()
302 >>= \dates_ -> case (dates, date2s) of -- NOTE: put hledger's date2 at least in second position
304 return $ context_date ctx:dates_
306 return $ Ledger.Posting_Typed type_ Posting
320 tags_of_comments :: [Comment] -> Tag.Tags
323 Map.unionsWith mappend
325 ( Data.Either.either (const Map.empty) id
326 . R.runParser (not_tag >> tags <* R.eof) () "" )
328 status :: Stream s m Char => ParsecT s u m Ledger.Status
331 R.skipMany $ R.space_horizontal
332 _ <- (R.char '*' <|> R.char '!')
337 -- | Return the 'Posting_Type' and stripped 'Account' of the given 'Account'.
338 posting_type :: Account -> (Ledger.Posting_Typed Account)
340 fromMaybe (Ledger.Posting_Typed Ledger.Posting_Type_Regular acct) $ do
343 case Text.stripPrefix virtual_begin name of
345 name'' <- liftM Text.strip $ Text.stripSuffix virtual_end name'
346 guard $ not $ Text.null name''
347 Just $ Ledger.Posting_Typed Ledger.Posting_Type_Virtual $ name'':|[]
349 name' <- liftM Text.strip $
350 Text.stripPrefix virtual_balanced_begin name
351 >>= Text.stripSuffix virtual_balanced_end
352 guard $ not $ Text.null name'
353 Just $ Ledger.Posting_Typed Ledger.Posting_Type_Virtual_Balanced $ name':|[]
354 first_name:|acct' -> do
355 let rev_acct' = List.reverse acct'
356 let last_name = List.head rev_acct'
357 case liftM Text.stripStart $
358 Text.stripPrefix virtual_begin first_name of
359 Just first_name' -> do
360 last_name' <- liftM Text.stripEnd $
361 Text.stripSuffix virtual_end last_name
362 guard $ not $ Text.null first_name'
363 guard $ not $ Text.null last_name'
364 Just $ Ledger.Posting_Typed
365 Ledger.Posting_Type_Virtual $
366 first_name':| List.reverse (last_name':List.tail rev_acct')
368 first_name' <- liftM Text.stripStart $
369 Text.stripPrefix virtual_balanced_begin first_name
370 last_name' <- liftM Text.stripEnd $
371 Text.stripSuffix virtual_balanced_end last_name
372 guard $ not $ Text.null first_name'
373 guard $ not $ Text.null last_name'
374 Just $ Ledger.Posting_Typed
375 Ledger.Posting_Type_Virtual_Balanced $
376 first_name':|List.reverse (last_name':List.tail rev_acct')
378 virtual_begin = Text.singleton posting_type_virtual_begin
379 virtual_end = Text.singleton posting_type_virtual_end
380 virtual_balanced_begin = Text.singleton posting_type_virtual_balanced_begin
381 virtual_balanced_end = Text.singleton posting_type_virtual_balanced_end
383 posting_type_virtual_begin :: Char
384 posting_type_virtual_begin = '('
385 posting_type_virtual_balanced_begin :: Char
386 posting_type_virtual_balanced_begin = '['
387 posting_type_virtual_end :: Char
388 posting_type_virtual_end = ')'
389 posting_type_virtual_balanced_end :: Char
390 posting_type_virtual_balanced_end = ']'
392 -- * Read 'Transaction'
395 ( Consable f ts (Chart_With t)
396 , Stream s (R.Error_State Error m) Char
398 ) => ParsecT s (Context f ts t) (R.Error_State Error m) Transaction
401 transaction_sourcepos <- R.getPosition
402 transaction_comments_before <-
406 _ -> return x <* R.new_line
407 date_ <- Date.Read.date Error_date (Just $ context_year ctx)
409 R.option [] $ R.try $ do
410 R.skipMany $ R.space_horizontal
412 R.skipMany $ R.space_horizontal
414 (Date.Read.date Error_date (Just $ context_year ctx)) $
416 R.many $ R.space_horizontal
418 >> (R.many $ R.space_horizontal)
419 let transaction_dates = (date_, dates_)
420 R.skipMany $ R.space_horizontal
421 transaction_status <- status
422 transaction_code <- R.option "" $ R.try code
423 R.skipMany $ R.space_horizontal
424 transaction_description <- description
425 R.skipMany $ R.space_horizontal
426 transaction_comments_after <- comments
427 let transaction_tags =
429 (tags_of_comments transaction_comments_before)
430 (tags_of_comments transaction_comments_after)
432 (postings_unchecked, postings_not_regular) <-
433 first (Ledger.map_Postings_by_Account . List.map
434 (\(Ledger.Posting_Typed _ p) -> p)) .
435 List.partition (\(Ledger.Posting_Typed pt _) ->
436 Ledger.Posting_Type_Regular == pt) <$>
437 R.many1_separated posting R.new_line
438 let (transaction_virtual_postings, balanced_virtual_postings_unchecked) =
439 join (***) (Ledger.map_Postings_by_Account . List.map
440 (\(Ledger.Posting_Typed _ p) -> p)) $
441 List.partition (\(Ledger.Posting_Typed pt _) ->
442 Ledger.Posting_Type_Virtual == pt)
447 , transaction_comments_before
448 , transaction_comments_after
450 , transaction_description
451 , transaction_postings=postings_unchecked
452 , transaction_virtual_postings
453 , transaction_balanced_virtual_postings=balanced_virtual_postings_unchecked
454 , transaction_sourcepos
458 let styles = Ledger.journal_amount_styles $ context_journal ctx
459 transaction_postings <-
460 case Balance.infer_equilibrium postings_unchecked of
461 (_, Left ko) -> R.fail_with "transaction infer_equilibrium" $
462 Error_transaction_not_equilibrated styles tr_unchecked ko
463 (_bal, Right ok) -> return ok
464 transaction_balanced_virtual_postings <-
465 case Balance.infer_equilibrium balanced_virtual_postings_unchecked of
466 (_, Left ko) -> R.fail_with "transaction infer_equilibrium" $
467 Error_virtual_transaction_not_equilibrated styles tr_unchecked ko
468 (_bal, Right ok) -> return ok
471 { transaction_postings
472 , transaction_balanced_virtual_postings
479 code :: (Consable f ts (Chart_With t), Stream s m Char)
480 => ParsecT s (Context f ts t) m Ledger.Code
483 R.skipMany $ R.space_horizontal
484 R.between (R.char '(') (R.char ')') $
485 R.many $ R.satisfy (\c -> c /= ')' && not (R.is_space_horizontal c))
488 description :: Stream s m Char => ParsecT s u m Ledger.Description
491 R.many $ R.try description_char
494 description_char :: Stream s m Char => ParsecT s u m Char
495 description_char = do
498 _ | c == comment_begin -> R.parserZero
499 _ | R.is_space_horizontal c -> return c <* (R.lookAhead $ R.try $ description_char)
500 _ | not (Data.Char.isSpace c) -> return c
506 :: (Consable f ts (Chart_With t), Stream s m Char)
507 => ParsecT s (Context f ts t) m ()
509 year <- R.integer_of_digits 10 <$> R.many1 R.digit
510 R.skipMany R.space_horizontal
511 context_ <- R.getState
512 R.setState context_{context_year=year}
515 default_unit_and_style
516 :: (Consable f ts (Chart_With t), Stream s m Char)
517 => ParsecT s (Context f ts t) m ()
518 default_unit_and_style = (do
519 (sty, amt) <- Ledger.Amount.Read.amount
520 R.skipMany R.space_horizontal
522 let unit = Amount.amount_unit amt
525 let jnl = context_journal ctx in
527 { Ledger.journal_amount_styles =
528 let Ledger.Amount.Style.Styles styles =
529 Ledger.journal_amount_styles jnl in
530 Ledger.Amount.Style.Styles $
531 Map.insertWith const unit sty styles
533 , context_unit = Just unit
535 ) <?> "default unit and style"
538 ( Consable f ts (Chart_With Transaction)
540 , Show (ts (Chart_With Transaction))
541 , Stream s (R.Error_State Error IO) Char
542 , NFData (ts (Chart_With Transaction))
544 => ParsecT s (Context f ts Transaction)
545 (R.Error_State Error IO)
548 sourcepos <- R.getPosition
549 filename <- R.manyTill R.anyChar (R.lookAhead (R.try R.new_line <|> R.eof))
550 context_including <- R.getState
551 let journal_including = context_journal context_including
552 let cwd = Path.takeDirectory (R.sourceName sourcepos)
553 file_path <- liftIO $ Path.abs cwd filename
555 join $ liftIO $ Exception.catch
556 (liftM return $ Text.IO.readFile file_path)
557 (return . R.fail_with "include reading" . Error_reading_file file_path)
558 (journal_included, context_included) <- do
560 R.runParserT_with_Error
561 (R.and_state $ journal_rec file_path)
566 journal_chart journal_including
571 Right ok -> return ok
572 Left ko -> R.fail_with "include parsing" (Error_including_file file_path ko)
578 journal_included{journal_file=file_path} :
579 journal_includes journal_including
581 journal_chart journal_included
589 ( Consable f ts (Chart_With Transaction)
591 , Show (ts (Chart_With Transaction))
592 , Stream s (R.Error_State Error IO) Char
594 => ParsecT s (Context f ts Transaction)
595 (R.Error_State Error IO)
598 -- sourcepos <- R.getPosition
599 acct <- Ledger.Account.Read.account
600 R.skipMany R.space_horizontal
603 tags_ <- R.many_separated
604 (R.skipMany1 R.space_horizontal >> tag
605 <* R.skipMany R.space_horizontal <* comments)
609 TreeMap.singleton acct $
611 Map.fromListWith (flip mappend) $
612 List.map (\(p, v) -> (p, [v])) tags_
616 (flip (\(p:|ps, v) ->
617 TreeMap.insert mappend
618 (p:|ps `mappend` [v])
624 let j = context_journal ctx
631 { Chart.chart_accounts
632 -- , Chart.chart_tags
641 ( Consable f ts (Chart_With Transaction)
643 , Show (ts (Chart_With Transaction))
644 , Stream s (R.Error_State Error IO) Char
645 , NFData (ts (Chart_With Transaction))
648 -> ParsecT s (Context f ts Transaction)
649 (R.Error_State Error IO)
650 (Journal (ts (Chart_With Transaction)))
652 currentLocalTime <- liftIO $
654 <$> Time.getCurrentTimeZone
655 <*> Time.getCurrentTime
656 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
658 R.setState $ ctx{context_year=currentLocalYear}
663 ( Consable f ts (Chart_With Transaction)
665 , Show (ts (Chart_With Transaction))
666 , Stream s (R.Error_State Error IO) Char
667 , NFData (ts (Chart_With Transaction))
670 -> ParsecT s (Context f ts Transaction)
671 (R.Error_State Error IO)
672 (Journal (ts (Chart_With Transaction)))
673 journal_rec file_ = do
674 last_read_time <- liftIO Date.now
682 journal_ <- context_journal <$> R.getState
685 { journal_file = file_
686 , journal_last_read_time = last_read_time
687 , journal_includes = List.reverse $ journal_includes journal_
692 => ParsecT s u m (ParsecT s u m ())
695 R.skipMany (R.skipMany R.space_horizontal >> R.new_line)
697 R.skipMany (R.skipMany R.space_horizontal >> R.new_line)
698 R.try (R.skipMany R.space_horizontal >> R.eof) <|> loop r
701 , Consable f ts (Chart_With Transaction)
703 , Show (ts (Chart_With Transaction))
704 , u ~ Context f ts Transaction
705 , m ~ R.Error_State Error IO
707 => ParsecT s u m (ParsecT s u m ())
710 _ <- R.lookAhead (R.try $ R.char comment_begin)
714 R.modifyState $ \ctx ->
715 let j = context_journal ctx in
718 mcons (context_filter ctx) cmts $
724 , Consable f ts (Chart_With Transaction)
726 , Show (ts (Chart_With Transaction))
727 , u ~ Context f ts Transaction
728 , m ~ R.Error_State Error IO
729 , NFData (ts (Chart_With Transaction))
731 => ParsecT s u m (ParsecT s u m ())
733 let choice s = R.string s >> R.skipMany1 R.space_horizontal
735 [ choice "Y" >> return default_year
736 , choice "D" >> return default_unit_and_style
737 , choice "!include" >> return include
741 , Consable f ts (Chart_With Transaction)
743 , Show (ts (Chart_With Transaction))
744 , u ~ Context f ts Transaction
745 , m ~ R.Error_State Error IO
746 , NFData (ts (Chart_With Transaction))
748 => ParsecT s u m (ParsecT s u m ())
749 jump_transaction = do
750 _ <- R.lookAhead $ R.try (R.many1 R.digit >> Date.Read.date_separator)
753 R.modifyState $ \ctx ->
754 let j = context_journal ctx in
759 (Chart_With (journal_chart j) t)
760 (journal_sections j)}}
763 , Consable f ts (Chart_With Transaction)
765 , Show (ts (Chart_With Transaction))
766 , u ~ Context f ts Transaction
767 , m ~ R.Error_State Error IO
769 => ParsecT s u m (ParsecT s u m ())
773 -- ** Read 'Journal' from a file
777 ( Consable f ts (Chart_With Transaction)
779 , Show (ts (Chart_With Transaction))
780 , NFData (ts (Chart_With Transaction))
782 => Context f ts Transaction
784 -> ExceptT [R.Error Error] IO (Journal (ts (Chart_With Transaction)))
788 (liftM Right $ Text.IO.readFile path) $
789 \ko -> return $ Left $
790 [ R.Error_Custom (R.initialPos path) $ Error_reading_file path ko ]
791 >>= liftIO . R.runParserT_with_Error (journal path) ctx path
793 Left ko -> throwE $ ko
794 Right ok -> ExceptT $ return $ Right ok