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
38 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
39 import qualified Text.Parsec.Pos as R
40 import qualified Data.Text.IO as Text.IO (readFile)
41 import qualified Data.Text as Text
42 import qualified System.FilePath.Posix as Path
44 import qualified Hcompta.Balance as Balance
45 import qualified Hcompta.Account as Account
46 import Hcompta.Account (Account)
47 import qualified Hcompta.Amount as Amount
48 import qualified Hcompta.Amount.Style as Style
49 import qualified Hcompta.Amount.Read as Amount.Read
50 import qualified Hcompta.Amount.Unit as Unit
51 import qualified Hcompta.Date as Date
52 import Hcompta.Date (Date)
53 import qualified Hcompta.Date.Read as Date.Read
54 import qualified Hcompta.Format.Ledger as Ledger
55 import Hcompta.Format.Ledger
58 , Posting(..), Posting_Type(..)
59 , Tag, Tag_Name, Tag_Value, Tag_by_Name
62 import Hcompta.Lib.Consable (Consable(..))
63 import qualified Hcompta.Lib.Regex as Regex
64 import Hcompta.Lib.Regex (Regex)
65 import qualified Hcompta.Lib.Parsec as R
66 import qualified Hcompta.Lib.Path as Path
70 { context_account_prefix :: !(Maybe Account)
71 , context_aliases_exact :: !(Data.Map.Map Account Account)
72 , context_aliases_joker :: ![(Account.Joker, Account)]
73 , context_aliases_regex :: ![(Regex, Account)]
74 , context_date :: !Date
75 , context_filter :: !f
76 , context_journal :: !(Journal (ts t))
77 , context_unit_and_style :: !(Maybe (Amount.Unit, Amount.Style))
78 , context_year :: !Date.Year
82 :: (Show f, Consable f ts t)
83 => f -> Journal (ts t) -> Context f ts t
84 context flt context_journal =
86 { context_account_prefix = Nothing
87 , context_aliases_exact = Data.Map.empty
88 , context_aliases_joker = []
89 , context_aliases_regex = []
90 , context_date = Date.nil
91 , context_filter = flt
93 , context_unit_and_style = Nothing
94 , context_year = Date.year Date.nil
98 = Error_date Date.Read.Error
99 | Error_transaction_not_equilibrated Transaction [Balance.Unit_Sum (Balance.Posting_Amount Ledger.Posting)]
100 | Error_virtual_transaction_not_equilibrated Transaction [Balance.Unit_Sum (Balance.Posting_Amount Ledger.Posting)]
101 | Error_reading_file FilePath Exception.IOException
102 | Error_including_file FilePath [R.Error Error]
107 account_name_sep :: Char
108 account_name_sep = ':'
110 -- | Read an 'Account'.
111 account :: Stream s m Char => ParsecT s u m Account
113 R.notFollowedBy $ R.space_horizontal
114 Account.from_List <$> do
115 R.many1_separated account_name $ R.char account_name_sep
117 -- | Read an Account.'Account.Name'.
118 account_name :: Stream s m Char => ParsecT s u m Account.Name
121 R.many1 $ R.try account_name_char
123 account_name_char :: Stream s m Char => ParsecT s u m Char
124 account_name_char = do
127 _ | c == comment_begin -> R.parserZero
128 _ | c == account_name_sep -> R.parserZero
129 _ | R.is_space_horizontal c -> do
130 _ <- R.notFollowedBy $ R.space_horizontal
131 return c <* (R.lookAhead $ R.try $
132 ( R.try (R.char account_name_sep)
133 <|> account_name_char
135 _ | not (Data.Char.isSpace c) -> return c
138 -- | Read an Account.'Account.Joker_Name'.
139 account_joker_name :: Stream s m Char => ParsecT s u m Account.Joker_Name
140 account_joker_name = do
141 n <- R.option Nothing $ (Just <$> account_name)
143 Nothing -> R.char account_name_sep >> (return $ Account.Joker_Any)
144 Just n' -> return $ Account.Joker_Name n'
146 -- | Read an Account.'Account.Joker'.
147 account_joker :: Stream s m Char => ParsecT s u m Account.Joker
149 R.notFollowedBy $ R.space_horizontal
150 R.many1_separated account_joker_name $ R.char account_name_sep
153 account_regex :: Stream s m Char => ParsecT s u m Regex
155 re <- R.many1 $ R.satisfy (not . R.is_space_horizontal)
158 -- | Read an Account.'Account.Filter'.
159 account_pattern :: Stream s m Char => ParsecT s u m Account.Pattern
162 [ Account.Pattern_Exact <$> (R.char '=' >> account)
163 , Account.Pattern_Joker <$> (R.char '*' >> account_joker)
164 , Account.Pattern_Regex <$> (R.option '~' (R.char '~') >> account_regex)
170 :: (Consable f ts t, Stream s m Char)
171 => ParsecT s (Context f ts t) m ()
173 _ <- R.string "alias"
174 R.skipMany1 $ R.space_horizontal
175 pattern <- account_pattern
176 R.skipMany $ R.space_horizontal
178 R.skipMany $ R.space_horizontal
180 R.skipMany $ R.space_horizontal
182 Account.Pattern_Exact acct -> R.modifyState $ \ctx -> ctx{context_aliases_exact=
183 Data.Map.insert acct repl $ context_aliases_exact ctx}
184 Account.Pattern_Joker jokr -> R.modifyState $ \ctx -> ctx{context_aliases_joker=
185 (jokr, repl):context_aliases_joker ctx}
186 Account.Pattern_Regex regx -> R.modifyState $ \ctx -> ctx{context_aliases_regex=
187 (regx, repl):context_aliases_regex ctx}
192 comment_begin :: Char
195 comment :: Stream s m Char => ParsecT s u m Comment
197 _ <- R.char comment_begin
199 R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
202 comments :: Stream s m Char => ParsecT s u m [Comment]
206 R.many1_separated comment (R.new_line >> R.skipMany R.space_horizontal)
212 tag_value_sep :: Char
219 tag :: Stream s m Char => ParsecT s u m Tag
222 _ <- R.char tag_value_sep
227 tag_name :: Stream s m Char => ParsecT s u m Tag_Name
230 R.many1 $ R.satisfy (\c -> c /= tag_value_sep && not (Data.Char.isSpace c))
232 tag_value :: Stream s m Char => ParsecT s u m Tag_Value
235 R.manyTill R.anyChar $ do
237 R.try (R.char tag_sep >> R.many R.space_horizontal >> tag_name >> void (R.char tag_value_sep))
241 tags :: Stream s m Char => ParsecT s u m Tag_by_Name
243 Ledger.tag_by_Name <$> do
244 R.many_separated tag $ do
246 R.skipMany $ R.space_horizontal
249 not_tag :: Stream s m Char => ParsecT s u m ()
251 R.skipMany $ R.try $ do
252 R.skipMany $ R.satisfy
253 (\c -> c /= tag_value_sep
254 && not (Data.Char.isSpace c))
260 :: (Consable f ts t, Stream s (R.Error_State Error m) Char, Monad m)
261 => ParsecT s (Context f ts t) (R.Error_State Error m) (Posting, Posting_Type)
264 sourcepos <- R.getPosition
265 R.skipMany1 $ R.space_horizontal
267 R.skipMany $ R.space_horizontal
269 let (type_, account_) = posting_type acct
273 _ <- R.count 2 R.space_horizontal
274 R.skipMany $ R.space_horizontal
276 if u == Unit.nil then id
278 Data.Map.adjust (\a ->
279 a{ Amount.style = s{Style.precision = Style.precision $ Amount.style a}
282 (context_unit_and_style ctx) .
283 Amount.from_List <$> do
284 R.many_separated Amount.Read.amount $ do
285 R.skipMany $ R.space_horizontal
286 _ <- R.char amount_sep
287 R.skipMany $ R.space_horizontal
289 , return Data.Map.empty
291 R.skipMany $ R.space_horizontal
292 -- TODO: balance assertion
294 comments_ <- comments
295 let tags_ = tags_of_comments comments_
297 case Data.Map.lookup "date" tags_ of
300 let date2s = Data.Map.lookup "date2" tags_ -- NOTE: support hledger's date2
302 forM (dates ++ fromMaybe [] date2s) $ \s ->
303 R.runParserT_with_Error_fail "tag date" id
304 (Date.Read.date Error_date (Just $ context_year ctx) <* R.eof) ()
306 >>= \dates_ -> case (dates, date2s) of -- NOTE: put hledger's date2 at least in second position
308 return $ context_date ctx:dates_
311 { posting_account=account_
312 , posting_amounts=amounts_
313 , posting_comments=comments_
314 , posting_dates=dates_
315 , posting_sourcepos=sourcepos
316 , posting_status=status_
324 tags_of_comments :: [Comment] -> Tag_by_Name
326 Data.Map.unionsWith (++)
328 ( Data.Either.either (const Data.Map.empty) id
329 . R.runParser (not_tag >> tags <* R.eof) () "" )
331 status :: Stream s m Char => ParsecT s u m Ledger.Status
334 R.skipMany $ R.space_horizontal
335 _ <- (R.char '*' <|> R.char '!')
340 -- | Return the 'Posting_Type' and stripped 'Account' of the given 'Account'.
341 posting_type :: Account -> (Posting_Type, Account)
343 fromMaybe (Posting_Type_Regular, acct) $ do
346 case Text.stripPrefix virtual_begin name of
348 name'' <- liftM Text.strip $ Text.stripSuffix virtual_end name'
349 guard $ not $ Text.null name''
350 Just (Posting_Type_Virtual, name'':|[])
352 name' <- liftM Text.strip $
353 Text.stripPrefix virtual_balanced_begin name
354 >>= Text.stripSuffix virtual_balanced_end
355 guard $ not $ Text.null name'
356 Just (Posting_Type_Virtual_Balanced, name':|[])
357 first_name:|acct' -> do
358 let rev_acct' = Data.List.reverse acct'
359 let last_name = Data.List.head rev_acct'
360 case liftM Text.stripStart $
361 Text.stripPrefix virtual_begin first_name of
362 Just first_name' -> do
363 last_name' <- liftM Text.stripEnd $
364 Text.stripSuffix virtual_end last_name
365 guard $ not $ Text.null first_name'
366 guard $ not $ Text.null last_name'
368 ( Posting_Type_Virtual
370 Data.List.reverse (last_name':Data.List.tail rev_acct')
373 first_name' <- liftM Text.stripStart $
374 Text.stripPrefix virtual_balanced_begin first_name
375 last_name' <- liftM Text.stripEnd $
376 Text.stripSuffix virtual_balanced_end last_name
377 guard $ not $ Text.null first_name'
378 guard $ not $ Text.null last_name'
380 ( Posting_Type_Virtual_Balanced
382 Data.List.reverse (last_name':Data.List.tail rev_acct')
385 virtual_begin = Text.singleton posting_type_virtual_begin
386 virtual_end = Text.singleton posting_type_virtual_end
387 virtual_balanced_begin = Text.singleton posting_type_virtual_balanced_begin
388 virtual_balanced_end = Text.singleton posting_type_virtual_balanced_end
390 posting_type_virtual_begin :: Char
391 posting_type_virtual_begin = '('
392 posting_type_virtual_balanced_begin :: Char
393 posting_type_virtual_balanced_begin = '['
394 posting_type_virtual_end :: Char
395 posting_type_virtual_end = ')'
396 posting_type_virtual_balanced_end :: Char
397 posting_type_virtual_balanced_end = ']'
399 -- * Read 'Transaction'
402 :: (Consable f ts t, Stream s (R.Error_State Error m) Char, Monad m)
403 => ParsecT s (Context f ts t) (R.Error_State Error m) Transaction
406 transaction_sourcepos <- R.getPosition
407 transaction_comments_before <-
411 _ -> return x <* R.new_line
412 date_ <- Date.Read.date Error_date (Just $ context_year ctx)
414 R.option [] $ R.try $ do
415 R.skipMany $ R.space_horizontal
417 R.skipMany $ R.space_horizontal
419 (Date.Read.date Error_date (Just $ context_year ctx)) $
421 R.many $ R.space_horizontal
423 >> (R.many $ R.space_horizontal)
424 let transaction_dates = (date_, dates_)
425 R.skipMany $ R.space_horizontal
426 transaction_status <- status
427 transaction_code <- R.option "" $ R.try code
428 R.skipMany $ R.space_horizontal
429 transaction_description <- description
430 R.skipMany $ R.space_horizontal
431 transaction_comments_after <- comments
432 let transaction_tags =
433 Data.Map.unionWith (++)
434 (tags_of_comments transaction_comments_before)
435 (tags_of_comments transaction_comments_after)
437 (postings_unchecked, postings_not_regular) <-
438 first (Ledger.posting_by_Account . Data.List.map fst) .
439 Data.List.partition ((Posting_Type_Regular ==) . snd) <$>
440 R.many1_separated posting R.new_line
441 let (transaction_virtual_postings, balanced_virtual_postings_unchecked) =
442 join (***) (Ledger.posting_by_Account . Data.List.map fst) $
443 Data.List.partition ((Posting_Type_Virtual ==) . snd)
448 , transaction_comments_before
449 , transaction_comments_after
451 , transaction_description
452 , transaction_postings=postings_unchecked
453 , transaction_virtual_postings
454 , transaction_balanced_virtual_postings=balanced_virtual_postings_unchecked
455 , transaction_sourcepos
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 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 tr_unchecked ko)
468 (_bal, Right ok) -> return ok
471 { transaction_postings
472 , transaction_balanced_virtual_postings
479 code :: (Consable f ts 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 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 >> R.new_line
511 context_ <- R.getState
512 R.setState context_{context_year=year}
515 default_unit_and_style
516 :: (Consable f ts t, Stream s m Char)
517 => ParsecT s (Context f ts t) m ()
518 default_unit_and_style = (do
519 amount_ <- Amount.Read.amount
520 R.skipMany R.space_horizontal >> R.new_line
521 context_ <- R.getState
522 R.setState context_{context_unit_and_style =
524 ( Amount.unit amount_
525 , Amount.style amount_ )}
526 ) <?> "default unit and style"
529 ( Consable f ts Transaction
531 , Show (ts Transaction)
532 , Stream s (R.Error_State Error IO) Char
534 => ParsecT s (Context f ts Transaction) (R.Error_State Error IO) ()
536 sourcepos <- R.getPosition
537 filename <- R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
538 context_ <- R.getState
539 let journal_ = context_journal context_
540 let cwd = Path.takeDirectory (R.sourceName sourcepos)
541 file_path <- liftIO $ Path.abs cwd filename
543 join $ liftIO $ Exception.catch
544 (liftM return $ readFile file_path)
545 (return . R.fail_with "include reading" . Error_reading_file file_path)
546 (journal_included, context_included) <- do
548 R.runParserT_with_Error (R.and_state $ journal_rec file_path)
549 context_{context_journal = Ledger.journal}
552 Right ok -> return ok
553 Left ko -> R.fail_with "include parsing" (Error_including_file file_path ko)
555 context_included{context_journal=
556 journal_{journal_includes=
557 journal_included{journal_file=file_path}
558 : journal_includes journal_}}
564 ( Consable f ts Transaction
566 , Show (ts Transaction)
567 , Stream s (R.Error_State Error IO) Char
570 -> ParsecT s (Context f ts Transaction) (R.Error_State Error IO) (Journal (ts Transaction))
572 currentLocalTime <- liftIO $
574 <$> Time.getCurrentTimeZone
575 <*> Time.getCurrentTime
576 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
578 R.setState $ ctx{context_year=currentLocalYear}
583 ( Consable f ts Transaction
585 , Show (ts Transaction)
586 , Stream s (R.Error_State Error IO) Char
589 -> ParsecT s (Context f ts Transaction) (R.Error_State Error IO) (Journal (ts Transaction))
590 journal_rec file_ = do
591 last_read_time <- liftIO Date.now
594 [ R.skipMany1 R.space
596 [ R.string "Y" >> return default_year
597 , R.string "D" >> return default_unit_and_style
598 , R.string "!include" >> return include
600 >>= \r -> R.skipMany1 R.space_horizontal >> r)
604 let j = context_journal ctx
607 j{journal_transactions=
608 mcons (context_filter ctx) t $
609 journal_transactions j}}
610 R.new_line <|> R.eof))
611 , R.try (void $ comment)
614 journal_ <- context_journal <$> R.getState
617 { journal_file = file_
618 , journal_last_read_time = last_read_time
619 , journal_includes = reverse $ journal_includes journal_
622 -- ** Read 'Journal' from a file
626 ( Consable f ts Transaction
628 , Show (ts Transaction)
630 => Context f ts Transaction
632 -> ExceptT [R.Error Error] IO (Journal (ts Transaction))
636 (liftM Right $ Text.IO.readFile path) $
637 \ko -> return $ Left $
638 [ R.Error_Custom (R.initialPos path) $ Error_reading_file path ko ]
639 >>= liftIO . R.runParserT_with_Error (journal path) ctx path
641 Left ko -> throwE $ ko
642 Right ok -> ExceptT $ return $ Right ok