1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE NamedFieldPuns #-}
3 {-# LANGUAGE OverloadedStrings #-}
4 {-# LANGUAGE ScopedTypeVariables #-}
5 {-# LANGUAGE TupleSections #-}
6 {-# LANGUAGE TypeFamilies #-}
7 module Hcompta.Format.Ledger.Read where
9 -- import Control.Applicative ((<$>), (<*>), (<*))
10 import qualified Control.Exception as Exception
11 import Control.Arrow ((***), first)
12 import Control.Monad (guard, join, liftM, forM, void)
13 import Control.Monad.IO.Class (liftIO)
14 import Control.Monad.Trans.Except (ExceptT(..), throwE)
15 import qualified Data.Char
16 import qualified Data.Either
17 import qualified Data.List
18 import Data.List.NonEmpty (NonEmpty(..))
19 import qualified Data.Map.Strict as Data.Map
20 import Data.Maybe (fromMaybe)
21 import Data.String (fromString)
22 import qualified Data.Time.Calendar as Time
23 import qualified Data.Time.Clock as Time
24 import qualified Data.Time.LocalTime as Time
25 import Data.Typeable ()
26 import qualified Text.Parsec as R hiding
39 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
40 import qualified Text.Parsec.Pos as R
41 import qualified Data.Text.IO as Text.IO (readFile)
42 import qualified Data.Text as Text
43 import qualified System.FilePath.Posix as Path
45 import qualified Hcompta.Balance as Balance
46 import qualified Hcompta.Account as Account
47 import Hcompta.Account (Account)
48 import qualified Hcompta.Amount as Amount
49 import qualified Hcompta.Amount.Style as Style
50 import qualified Hcompta.Amount.Read as Amount.Read
51 import qualified Hcompta.Amount.Unit as Unit
52 import qualified Hcompta.Date as Date
53 import Hcompta.Date (Date)
54 import qualified Hcompta.Date.Read as Date.Read
55 import qualified Hcompta.Format.Ledger as Ledger
56 import Hcompta.Format.Ledger
59 , Posting(..), Posting_Type(..)
60 , Tag, Tag_Name, Tag_Value, Tag_by_Name
63 import Hcompta.Lib.Consable (Consable(..))
64 import qualified Hcompta.Lib.Regex as Regex
65 import Hcompta.Lib.Regex (Regex)
66 import qualified Hcompta.Lib.Parsec as R
67 import qualified Hcompta.Lib.Path as Path
71 { context_account_prefix :: !(Maybe Account)
72 , context_aliases_exact :: !(Data.Map.Map Account Account)
73 , context_aliases_joker :: ![(Account.Joker, Account)]
74 , context_aliases_regex :: ![(Regex, Account)]
75 , context_date :: !Date
76 , context_filter :: !f
77 , context_journal :: !(Journal (ts t))
78 , context_unit_and_style :: !(Maybe (Amount.Unit, Amount.Style))
79 , context_year :: !Date.Year
83 :: (Show f, Consable f ts t)
84 => f -> Journal (ts t) -> Context f ts t
85 context flt context_journal =
87 { context_account_prefix = Nothing
88 , context_aliases_exact = Data.Map.empty
89 , context_aliases_joker = []
90 , context_aliases_regex = []
91 , context_date = Date.nil
92 , context_filter = flt
94 , context_unit_and_style = Nothing
95 , context_year = Date.year Date.nil
99 = Error_date Date.Read.Error
100 | Error_transaction_not_equilibrated Transaction [Balance.Unit_Sum (Balance.Posting_Amount Ledger.Posting)]
101 | Error_virtual_transaction_not_equilibrated Transaction [Balance.Unit_Sum (Balance.Posting_Amount Ledger.Posting)]
102 | Error_reading_file FilePath Exception.IOException
103 | Error_including_file FilePath [R.Error Error]
108 account_name_sep :: Char
109 account_name_sep = ':'
111 -- | Read an 'Account'.
112 account :: Stream s m Char => ParsecT s u m Account
114 R.notFollowedBy $ R.space_horizontal
115 Account.from_List <$> do
116 R.many1_separated account_name $ R.char account_name_sep
118 -- | Read an Account.'Account.Name'.
119 account_name :: Stream s m Char => ParsecT s u m Account.Name
122 R.many1 $ R.try account_name_char
124 account_name_char :: Stream s m Char => ParsecT s u m Char
125 account_name_char = do
128 _ | c == comment_begin -> R.parserZero
129 _ | c == account_name_sep -> R.parserZero
130 _ | c /= '\t' && R.is_space_horizontal c -> do
131 _ <- R.notFollowedBy $ R.space_horizontal
132 return c <* (R.lookAhead $ R.try $
133 ( R.try (R.char account_name_sep)
134 <|> account_name_char
136 _ | not (Data.Char.isSpace c) -> return c
139 -- | Read an Account.'Account.Joker_Name'.
140 account_joker_name :: Stream s m Char => ParsecT s u m Account.Joker_Name
141 account_joker_name = do
142 n <- R.option Nothing $ (Just <$> account_name)
144 Nothing -> R.char account_name_sep >> (return $ Account.Joker_Any)
145 Just n' -> return $ Account.Joker_Name n'
147 -- | Read an Account.'Account.Joker'.
148 account_joker :: Stream s m Char => ParsecT s u m Account.Joker
150 R.notFollowedBy $ R.space_horizontal
151 R.many1_separated account_joker_name $ R.char account_name_sep
154 account_regex :: Stream s m Char => ParsecT s u m Regex
156 re <- R.many1 $ R.satisfy (not . R.is_space_horizontal)
159 -- | Read an Account.'Account.Filter'.
160 account_pattern :: Stream s m Char => ParsecT s u m Account.Pattern
163 [ Account.Pattern_Exact <$> (R.char '=' >> account)
164 , Account.Pattern_Joker <$> (R.char '*' >> account_joker)
165 , Account.Pattern_Regex <$> (R.option '~' (R.char '~') >> account_regex)
171 :: (Consable f ts t, Stream s m Char)
172 => ParsecT s (Context f ts t) m ()
174 _ <- R.string "alias"
175 R.skipMany1 $ R.space_horizontal
176 pattern <- account_pattern
177 R.skipMany $ R.space_horizontal
179 R.skipMany $ R.space_horizontal
181 R.skipMany $ R.space_horizontal
183 Account.Pattern_Exact acct -> R.modifyState $ \ctx -> ctx{context_aliases_exact=
184 Data.Map.insert acct repl $ context_aliases_exact ctx}
185 Account.Pattern_Joker jokr -> R.modifyState $ \ctx -> ctx{context_aliases_joker=
186 (jokr, repl):context_aliases_joker ctx}
187 Account.Pattern_Regex regx -> R.modifyState $ \ctx -> ctx{context_aliases_regex=
188 (regx, repl):context_aliases_regex ctx}
193 comment_begin :: Char
196 comment :: Stream s m Char => ParsecT s u m Comment
198 _ <- R.char comment_begin
200 R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
203 comments :: Stream s m Char => ParsecT s u m [Comment]
207 R.many1_separated comment (R.new_line >> R.skipMany R.space_horizontal)
213 tag_value_sep :: Char
220 tag :: Stream s m Char => ParsecT s u m Tag
223 _ <- R.char tag_value_sep
228 tag_name :: Stream s m Char => ParsecT s u m Tag_Name
231 R.many1 $ R.satisfy (\c -> c /= tag_value_sep && not (Data.Char.isSpace c))
233 tag_value :: Stream s m Char => ParsecT s u m Tag_Value
236 R.manyTill R.anyChar $ do
238 R.try (R.char tag_sep >> R.many R.space_horizontal >> tag_name >> void (R.char tag_value_sep))
242 tags :: Stream s m Char => ParsecT s u m Tag_by_Name
244 Ledger.tag_by_Name <$> do
245 R.many_separated tag $ do
247 R.skipMany $ R.space_horizontal
250 not_tag :: Stream s m Char => ParsecT s u m ()
252 R.skipMany $ R.try $ do
253 R.skipMany $ R.satisfy
254 (\c -> c /= tag_value_sep
255 && not (Data.Char.isSpace c))
261 :: (Consable f ts t, Stream s (R.Error_State Error m) Char, Monad m)
262 => ParsecT s (Context f ts t) (R.Error_State Error m) (Posting, Posting_Type)
265 sourcepos <- R.getPosition
266 R.skipMany1 $ R.space_horizontal
268 R.skipMany $ R.space_horizontal
270 let (type_, account_) = posting_type acct
274 (void R.tab <|> void (R.count 2 R.space_horizontal))
275 R.skipMany $ R.space_horizontal
277 if u == Unit.nil then id
279 Data.Map.adjust (\a ->
280 a{ Amount.style = s{Style.precision = Style.precision $ Amount.style a}
283 (context_unit_and_style ctx) .
284 Amount.from_List <$> do
285 R.many_separated Amount.Read.amount $ do
286 R.skipMany $ R.space_horizontal
287 _ <- R.char amount_sep
288 R.skipMany $ R.space_horizontal
290 , return Data.Map.empty
292 R.skipMany $ R.space_horizontal
293 -- TODO: balance assertion
295 comments_ <- comments
296 let tags_ = tags_of_comments comments_
298 case Data.Map.lookup "date" tags_ of
301 let date2s = Data.Map.lookup "date2" tags_ -- NOTE: support hledger's date2
303 forM (dates ++ fromMaybe [] date2s) $ \s ->
304 R.runParserT_with_Error_fail "tag date" id
305 (Date.Read.date Error_date (Just $ context_year ctx) <* R.eof) ()
307 >>= \dates_ -> case (dates, date2s) of -- NOTE: put hledger's date2 at least in second position
309 return $ context_date ctx:dates_
312 { posting_account=account_
313 , posting_amounts=amounts_
314 , posting_comments=comments_
315 , posting_dates=dates_
316 , posting_sourcepos=sourcepos
317 , posting_status=status_
325 tags_of_comments :: [Comment] -> Tag_by_Name
327 Data.Map.unionsWith (++)
329 ( Data.Either.either (const Data.Map.empty) id
330 . R.runParser (not_tag >> tags <* R.eof) () "" )
332 status :: Stream s m Char => ParsecT s u m Ledger.Status
335 R.skipMany $ R.space_horizontal
336 _ <- (R.char '*' <|> R.char '!')
341 -- | Return the 'Posting_Type' and stripped 'Account' of the given 'Account'.
342 posting_type :: Account -> (Posting_Type, Account)
344 fromMaybe (Posting_Type_Regular, acct) $ do
347 case Text.stripPrefix virtual_begin name of
349 name'' <- liftM Text.strip $ Text.stripSuffix virtual_end name'
350 guard $ not $ Text.null name''
351 Just (Posting_Type_Virtual, name'':|[])
353 name' <- liftM Text.strip $
354 Text.stripPrefix virtual_balanced_begin name
355 >>= Text.stripSuffix virtual_balanced_end
356 guard $ not $ Text.null name'
357 Just (Posting_Type_Virtual_Balanced, name':|[])
358 first_name:|acct' -> do
359 let rev_acct' = Data.List.reverse acct'
360 let last_name = Data.List.head rev_acct'
361 case liftM Text.stripStart $
362 Text.stripPrefix virtual_begin first_name of
363 Just first_name' -> do
364 last_name' <- liftM Text.stripEnd $
365 Text.stripSuffix virtual_end last_name
366 guard $ not $ Text.null first_name'
367 guard $ not $ Text.null last_name'
369 ( Posting_Type_Virtual
371 Data.List.reverse (last_name':Data.List.tail rev_acct')
374 first_name' <- liftM Text.stripStart $
375 Text.stripPrefix virtual_balanced_begin first_name
376 last_name' <- liftM Text.stripEnd $
377 Text.stripSuffix virtual_balanced_end last_name
378 guard $ not $ Text.null first_name'
379 guard $ not $ Text.null last_name'
381 ( Posting_Type_Virtual_Balanced
383 Data.List.reverse (last_name':Data.List.tail rev_acct')
386 virtual_begin = Text.singleton posting_type_virtual_begin
387 virtual_end = Text.singleton posting_type_virtual_end
388 virtual_balanced_begin = Text.singleton posting_type_virtual_balanced_begin
389 virtual_balanced_end = Text.singleton posting_type_virtual_balanced_end
391 posting_type_virtual_begin :: Char
392 posting_type_virtual_begin = '('
393 posting_type_virtual_balanced_begin :: Char
394 posting_type_virtual_balanced_begin = '['
395 posting_type_virtual_end :: Char
396 posting_type_virtual_end = ')'
397 posting_type_virtual_balanced_end :: Char
398 posting_type_virtual_balanced_end = ']'
400 -- * Read 'Transaction'
403 :: (Consable f ts t, Stream s (R.Error_State Error m) Char, Monad m)
404 => ParsecT s (Context f ts t) (R.Error_State Error m) Transaction
407 transaction_sourcepos <- R.getPosition
408 transaction_comments_before <-
412 _ -> return x <* R.new_line
413 date_ <- Date.Read.date Error_date (Just $ context_year ctx)
415 R.option [] $ R.try $ do
416 R.skipMany $ R.space_horizontal
418 R.skipMany $ R.space_horizontal
420 (Date.Read.date Error_date (Just $ context_year ctx)) $
422 R.many $ R.space_horizontal
424 >> (R.many $ R.space_horizontal)
425 let transaction_dates = (date_, dates_)
426 R.skipMany $ R.space_horizontal
427 transaction_status <- status
428 transaction_code <- R.option "" $ R.try code
429 R.skipMany $ R.space_horizontal
430 transaction_description <- description
431 R.skipMany $ R.space_horizontal
432 transaction_comments_after <- comments
433 let transaction_tags =
434 Data.Map.unionWith (++)
435 (tags_of_comments transaction_comments_before)
436 (tags_of_comments transaction_comments_after)
438 (postings_unchecked, postings_not_regular) <-
439 first (Ledger.posting_by_Account . Data.List.map fst) .
440 Data.List.partition ((Posting_Type_Regular ==) . snd) <$>
441 R.many1_separated posting R.new_line
442 let (transaction_virtual_postings, balanced_virtual_postings_unchecked) =
443 join (***) (Ledger.posting_by_Account . Data.List.map fst) $
444 Data.List.partition ((Posting_Type_Virtual ==) . snd)
449 , transaction_comments_before
450 , transaction_comments_after
452 , transaction_description
453 , transaction_postings=postings_unchecked
454 , transaction_virtual_postings
455 , transaction_balanced_virtual_postings=balanced_virtual_postings_unchecked
456 , transaction_sourcepos
460 transaction_postings <-
461 case Balance.infer_equilibrium postings_unchecked of
462 (_, Left ko) -> R.fail_with "transaction infer_equilibrium"
463 (Error_transaction_not_equilibrated tr_unchecked ko)
464 (_bal, Right ok) -> return ok
465 transaction_balanced_virtual_postings <-
466 case Balance.infer_equilibrium balanced_virtual_postings_unchecked of
467 (_, Left ko) -> R.fail_with "transaction infer_equilibrium"
468 (Error_virtual_transaction_not_equilibrated tr_unchecked ko)
469 (_bal, Right ok) -> return ok
472 { transaction_postings
473 , transaction_balanced_virtual_postings
480 code :: (Consable f ts t, Stream s m Char)
481 => ParsecT s (Context f ts t) m Ledger.Code
484 R.skipMany $ R.space_horizontal
485 R.between (R.char '(') (R.char ')') $
486 R.many $ R.satisfy (\c -> c /= ')' && not (R.is_space_horizontal c))
489 description :: Stream s m Char => ParsecT s u m Ledger.Description
492 R.many $ R.try description_char
495 description_char :: Stream s m Char => ParsecT s u m Char
496 description_char = do
499 _ | c == comment_begin -> R.parserZero
500 _ | R.is_space_horizontal c -> return c <* (R.lookAhead $ R.try $ description_char)
501 _ | not (Data.Char.isSpace c) -> return c
507 :: (Consable f ts t, Stream s m Char)
508 => ParsecT s (Context f ts t) m ()
510 year <- R.integer_of_digits 10 <$> R.many1 R.digit
511 R.skipMany R.space_horizontal >> R.new_line
512 context_ <- R.getState
513 R.setState context_{context_year=year}
516 default_unit_and_style
517 :: (Consable f ts t, Stream s m Char)
518 => ParsecT s (Context f ts t) m ()
519 default_unit_and_style = (do
520 amount_ <- Amount.Read.amount
521 R.skipMany R.space_horizontal >> R.new_line
522 context_ <- R.getState
523 R.setState context_{context_unit_and_style =
525 ( Amount.unit amount_
526 , Amount.style amount_ )}
527 ) <?> "default unit and style"
530 ( Consable f ts Transaction
532 , Show (ts Transaction)
533 , Stream s (R.Error_State Error IO) Char
535 => ParsecT s (Context f ts Transaction) (R.Error_State Error IO) ()
537 sourcepos <- R.getPosition
538 filename <- R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
539 context_ <- R.getState
540 let journal_ = context_journal context_
541 let cwd = Path.takeDirectory (R.sourceName sourcepos)
542 file_path <- liftIO $ Path.abs cwd filename
544 join $ liftIO $ Exception.catch
545 (liftM return $ readFile file_path)
546 (return . R.fail_with "include reading" . Error_reading_file file_path)
547 (journal_included, context_included) <- do
549 R.runParserT_with_Error (R.and_state $ journal_rec file_path)
550 context_{context_journal = Ledger.journal}
553 Right ok -> return ok
554 Left ko -> R.fail_with "include parsing" (Error_including_file file_path ko)
556 context_included{context_journal=
557 journal_{journal_includes=
558 journal_included{journal_file=file_path}
559 : journal_includes journal_}}
565 ( Consable f ts Transaction
567 , Show (ts Transaction)
568 , Stream s (R.Error_State Error IO) Char
571 -> ParsecT s (Context f ts Transaction) (R.Error_State Error IO) (Journal (ts Transaction))
573 currentLocalTime <- liftIO $
575 <$> Time.getCurrentTimeZone
576 <*> Time.getCurrentTime
577 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
579 R.setState $ ctx{context_year=currentLocalYear}
584 ( Consable f ts Transaction
586 , Show (ts Transaction)
587 , Stream s (R.Error_State Error IO) Char
590 -> ParsecT s (Context f ts Transaction) (R.Error_State Error IO) (Journal (ts Transaction))
591 journal_rec file_ = do
592 last_read_time <- liftIO Date.now
595 [ R.skipMany1 R.space
597 [ R.string "Y" >> return default_year
598 , R.string "D" >> return default_unit_and_style
599 , R.string "!include" >> return include
601 >>= \r -> R.skipMany1 R.space_horizontal >> r)
605 let j = context_journal ctx
608 j{journal_transactions=
609 mcons (context_filter ctx) t $
610 journal_transactions j}}
611 R.new_line <|> R.eof))
612 , R.try (void $ comment)
615 journal_ <- context_journal <$> R.getState
618 { journal_file = file_
619 , journal_last_read_time = last_read_time
620 , journal_includes = reverse $ journal_includes journal_
623 -- ** Read 'Journal' from a file
627 ( Consable f ts Transaction
629 , Show (ts Transaction)
631 => Context f ts Transaction
633 -> ExceptT [R.Error Error] IO (Journal (ts Transaction))
637 (liftM Right $ Text.IO.readFile path) $
638 \ko -> return $ Left $
639 [ R.Error_Custom (R.initialPos path) $ Error_reading_file path ko ]
640 >>= liftIO . R.runParserT_with_Error (journal path) ctx path
642 Left ko -> throwE $ ko
643 Right ok -> ExceptT $ return $ Right ok