1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE FlexibleContexts #-}
3 {-# LANGUAGE NamedFieldPuns #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE ScopedTypeVariables #-}
6 {-# LANGUAGE TupleSections #-}
7 {-# LANGUAGE TypeFamilies #-}
8 module Hcompta.Format.Ledger.Read where
10 -- import Control.Applicative ((<$>), (<*>), (<*))
11 import qualified Control.Exception as Exception
12 import Control.Arrow ((***))
13 import Control.Monad (guard, join, liftM)
14 import Control.Monad.IO.Class (liftIO)
15 import Control.Monad.Trans.Except (ExceptT(..), throwE)
16 import Control.Monad.Trans.Class (lift)
17 import qualified Data.Char
18 import qualified Data.Either
19 import qualified Data.List
20 import Data.List.NonEmpty (NonEmpty(..))
21 import qualified Data.Map.Strict as Data.Map
22 import Data.Maybe (fromMaybe)
23 import Data.String (fromString)
24 import qualified Data.Time.Calendar as Time
25 import qualified Data.Time.Clock as Time
26 import qualified Data.Time.LocalTime as Time
27 import Data.Typeable ()
28 import qualified Text.Parsec as R hiding
40 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
41 import qualified Text.Parsec.Pos as R
42 import qualified Data.Text.IO as Text.IO (readFile)
43 import qualified Data.Text as Text
44 import qualified System.FilePath.Posix as Path
46 import qualified Hcompta.Balance as Balance
47 import qualified Hcompta.Account as Account
48 import Hcompta.Account (Account)
49 import qualified Hcompta.Amount as Amount
50 import qualified Hcompta.Amount.Style as Style
51 import qualified Hcompta.Amount.Read as Amount.Read
52 import qualified Hcompta.Amount.Unit as Unit
53 import qualified Hcompta.Date as Date
54 import Hcompta.Date (Date)
55 import qualified Hcompta.Date.Read as Date.Read
56 import qualified Hcompta.Format.Ledger as Ledger
57 import Hcompta.Format.Ledger
60 , Posting(..), Posting_Type(..)
61 , Tag, Tag_Name, Tag_Value, Tag_by_Name
64 import Hcompta.Lib.Consable (Consable(..))
65 import qualified Hcompta.Lib.Regex as Regex
66 import Hcompta.Lib.Regex (Regex)
67 import qualified Hcompta.Lib.Parsec as R
68 import qualified Hcompta.Lib.Path as Path
72 { context_account_prefix :: !(Maybe Account)
73 , context_aliases_exact :: !(Data.Map.Map Account Account)
74 , context_aliases_joker :: ![(Account.Joker, Account)]
75 , context_aliases_regex :: ![(Regex, Account)]
76 , context_date :: !Date
77 , context_unit_and_style :: !(Maybe (Amount.Unit, Amount.Style))
78 , context_journal :: !(Journal ts t)
79 , context_year :: !Date.Year
82 context :: Consable ts t => Journal ts t -> Context ts t
83 context context_journal =
85 { context_account_prefix = Nothing
86 , context_aliases_exact = Data.Map.empty
87 , context_aliases_joker = []
88 , context_aliases_regex = []
89 , context_date = Date.nil
90 , context_unit_and_style = Nothing
92 , context_year = Date.year Date.nil
96 = Error_date Date.Read.Error
97 | Error_transaction_not_equilibrated Transaction [Balance.Unit_Sum (Balance.Posting_Amount Ledger.Posting)]
98 | Error_virtual_transaction_not_equilibrated Transaction [Balance.Unit_Sum (Balance.Posting_Amount Ledger.Posting)]
99 | Error_reading_file FilePath Exception.IOException
100 | Error_including_file FilePath [R.Error Error]
105 account_name_sep :: Char
106 account_name_sep = ':'
108 -- | Read an 'Account'.
109 account :: Stream s m Char => ParsecT s u m Account
111 R.notFollowedBy $ R.space_horizontal
112 Account.from_List <$> do
113 R.many1_separated account_name $ R.char account_name_sep
115 -- | Read an Account.'Account.Name'.
116 account_name :: Stream s m Char => ParsecT s u m Account.Name
119 R.many1 $ R.try account_name_char
121 account_name_char :: Stream s m Char => ParsecT s u m Char
122 account_name_char = do
125 _ | c == comment_begin -> R.parserZero
126 _ | c == account_name_sep -> R.parserZero
127 _ | R.is_space_horizontal c -> do
128 _ <- R.notFollowedBy $ R.space_horizontal
129 return c <* (R.lookAhead $ R.try $
130 ( R.try (R.char account_name_sep)
131 <|> account_name_char
133 _ | not (Data.Char.isSpace c) -> return c
136 -- | Read an Account.'Account.Joker_Name'.
137 account_joker_name :: Stream s m Char => ParsecT s u m Account.Joker_Name
138 account_joker_name = do
139 n <- R.option Nothing $ (Just <$> account_name)
141 Nothing -> R.char account_name_sep >> (return $ Account.Joker_Any)
142 Just n' -> return $ Account.Joker_Name n'
144 -- | Read an Account.'Account.Joker'.
145 account_joker :: Stream s m Char => ParsecT s u m Account.Joker
147 R.notFollowedBy $ R.space_horizontal
148 R.many1_separated account_joker_name $ R.char account_name_sep
151 account_regex :: Stream s m Char => ParsecT s u m Regex
153 re <- R.many1 $ R.satisfy (not . R.is_space_horizontal)
156 -- | Read an Account.'Account.Filter'.
157 account_pattern :: Stream s m Char => ParsecT s u m Account.Pattern
160 [ Account.Pattern_Exact <$> (R.char '=' >> account)
161 , Account.Pattern_Joker <$> (R.char '*' >> account_joker)
162 , Account.Pattern_Regex <$> (R.option '~' (R.char '~') >> account_regex)
167 directive_alias :: (Consable ts t, Stream s m Char) => ParsecT s (Context ts t) m ()
169 _ <- R.string "alias"
170 R.skipMany1 $ R.space_horizontal
171 pattern <- account_pattern
172 R.skipMany $ R.space_horizontal
174 R.skipMany $ R.space_horizontal
176 R.skipMany $ R.space_horizontal
178 Account.Pattern_Exact acct -> R.modifyState $ \ctx -> ctx{context_aliases_exact=
179 Data.Map.insert acct repl $ context_aliases_exact ctx}
180 Account.Pattern_Joker jokr -> R.modifyState $ \ctx -> ctx{context_aliases_joker=
181 (jokr, repl):context_aliases_joker ctx}
182 Account.Pattern_Regex regx -> R.modifyState $ \ctx -> ctx{context_aliases_regex=
183 (regx, repl):context_aliases_regex ctx}
188 comment_begin :: Char
191 comment :: Stream s m Char => ParsecT s u m Comment
193 _ <- R.char comment_begin
195 R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
198 comments :: Stream s m Char => ParsecT s u m [Comment]
202 R.many1_separated comment (R.new_line >> R.skipMany R.space_horizontal)
208 tag_value_sep :: Char
215 tag :: Stream s m Char => ParsecT s u m Tag
218 _ <- R.char tag_value_sep
223 tag_name :: Stream s m Char => ParsecT s u m Tag_Name
226 R.many1 $ R.satisfy (\c -> c /= tag_value_sep && not (Data.Char.isSpace c))
228 tag_value :: Stream s m Char => ParsecT s u m Tag_Value
231 R.manyTill R.anyChar $ do
233 R.try (R.char tag_sep >> R.many R.space_horizontal >> tag_name >> R.char tag_value_sep >> return ())
237 tags :: Stream s m Char => ParsecT s u m Tag_by_Name
239 Ledger.tag_by_Name <$> do
240 R.many_separated tag $ do
242 R.skipMany $ R.space_horizontal
245 not_tag :: Stream s m Char => ParsecT s u m ()
247 R.skipMany $ R.try $ do
248 R.skipMany $ R.satisfy
249 (\c -> c /= tag_value_sep
250 && not (Data.Char.isSpace c))
256 :: (Consable ts t, Stream s (R.Error_State Error m) Char, Monad m)
257 => ParsecT s (Context ts t) (R.Error_State Error m) (Posting, Posting_Type)
260 sourcepos <- R.getPosition
261 R.skipMany1 $ R.space_horizontal
263 R.skipMany $ R.space_horizontal
265 let (type_, account_) = posting_type acct
269 _ <- R.count 2 R.space_horizontal
270 R.skipMany $ R.space_horizontal
272 if u == Unit.nil then id
274 Data.Map.adjust (\a ->
275 a{ Amount.style = s{Style.precision = Style.precision $ Amount.style a}
278 (context_unit_and_style ctx) .
279 Amount.from_List <$> do
280 R.many_separated Amount.Read.amount $ do
281 R.skipMany $ R.space_horizontal
282 _ <- R.char amount_sep
283 R.skipMany $ R.space_horizontal
285 , return Data.Map.empty
287 R.skipMany $ R.space_horizontal
288 -- TODO: balance assertion
290 comments_ <- comments
291 let tags_ = tags_of_comments comments_
293 case Data.Map.lookup "date" tags_ of
296 let date2s = Data.Map.lookup "date2" tags_ -- NOTE: support hledger's date2
298 (flip mapM) (dates ++ 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_
307 { posting_account=account_
308 , posting_amounts=amounts_
309 , posting_comments=comments_
310 , posting_dates=dates_
311 , posting_sourcepos=sourcepos
312 , posting_status=status_
320 tags_of_comments :: [Comment] -> Tag_by_Name
322 Data.Map.unionsWith (++)
324 ( Data.Either.either (const Data.Map.empty) id
325 . R.runParser (not_tag >> tags <* R.eof) () "" )
327 status :: Stream s m Char => ParsecT s u m Ledger.Status
330 R.skipMany $ R.space_horizontal
331 _ <- (R.char '*' <|> R.char '!')
336 -- | Return the 'Posting_Type' and stripped 'Account' of the given 'Account'.
337 posting_type :: Account -> (Posting_Type, Account)
339 fromMaybe (Posting_Type_Regular, acct) $ do
342 case Text.stripPrefix virtual_begin name of
345 Text.stripSuffix virtual_end name'
346 >>= return . Text.strip
347 guard $ not $ Text.null name''
348 Just (Posting_Type_Virtual, name'':|[])
351 Text.stripPrefix virtual_balanced_begin name
352 >>= Text.stripSuffix virtual_balanced_end
353 >>= return . Text.strip
354 guard $ not $ Text.null name'
355 Just (Posting_Type_Virtual_Balanced, name':|[])
356 first_name:|acct' -> do
357 let rev_acct' = Data.List.reverse acct'
358 let last_name = Data.List.head rev_acct'
359 case Text.stripPrefix virtual_begin first_name
360 >>= return . Text.stripStart of
361 Just first_name' -> do
363 Text.stripSuffix virtual_end last_name
364 >>= return . Text.stripEnd
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')
374 Text.stripPrefix virtual_balanced_begin first_name
375 >>= return . Text.stripStart
377 Text.stripSuffix virtual_balanced_end last_name
378 >>= return . Text.stripEnd
379 guard $ not $ Text.null first_name'
380 guard $ not $ Text.null last_name'
382 ( Posting_Type_Virtual_Balanced
384 Data.List.reverse (last_name':Data.List.tail rev_acct')
387 virtual_begin = Text.singleton posting_type_virtual_begin
388 virtual_end = Text.singleton posting_type_virtual_end
389 virtual_balanced_begin = Text.singleton posting_type_virtual_balanced_begin
390 virtual_balanced_end = Text.singleton posting_type_virtual_balanced_end
392 posting_type_virtual_begin :: Char
393 posting_type_virtual_begin = '('
394 posting_type_virtual_balanced_begin :: Char
395 posting_type_virtual_balanced_begin = '['
396 posting_type_virtual_end :: Char
397 posting_type_virtual_end = ')'
398 posting_type_virtual_balanced_end :: Char
399 posting_type_virtual_balanced_end = ']'
401 -- * Read 'Transaction'
404 :: (Consable ts t, Stream s (R.Error_State Error m) Char, Monad m)
405 => ParsecT s (Context ts t) (R.Error_State Error m) Transaction
408 transaction_sourcepos <- R.getPosition
409 transaction_comments_before <-
413 _ -> return x <* R.new_line
414 date_ <- Date.Read.date Error_date (Just $ context_year ctx)
416 R.option [] $ R.try $ do
417 R.skipMany $ R.space_horizontal
419 R.skipMany $ R.space_horizontal
421 (Date.Read.date Error_date (Just $ context_year ctx)) $
423 R.many $ R.space_horizontal
425 >> (R.many $ R.space_horizontal)
426 let transaction_dates = (date_, dates_)
427 R.skipMany $ R.space_horizontal
428 transaction_status <- status
429 transaction_code <- R.option "" $ R.try code
430 R.skipMany $ R.space_horizontal
431 transaction_description <- description
432 R.skipMany $ R.space_horizontal
433 transaction_comments_after <- comments
434 let transaction_tags =
435 Data.Map.unionWith (++)
436 (tags_of_comments transaction_comments_before)
437 (tags_of_comments transaction_comments_after)
439 (postings_unchecked, postings_not_regular) <-
440 ((Ledger.posting_by_Account . Data.List.map fst) *** id) .
441 Data.List.partition ((Posting_Type_Regular ==) . snd) <$>
442 R.many1_separated posting R.new_line
443 let (transaction_virtual_postings, balanced_virtual_postings_unchecked) =
444 join (***) (Ledger.posting_by_Account . Data.List.map fst) $
445 Data.List.partition ((Posting_Type_Virtual ==) . snd)
450 , transaction_comments_before
451 , transaction_comments_after
453 , transaction_description
454 , transaction_postings=postings_unchecked
455 , transaction_virtual_postings
456 , transaction_balanced_virtual_postings=balanced_virtual_postings_unchecked
457 , transaction_sourcepos
461 transaction_postings <-
462 case Balance.infer_equilibrium postings_unchecked of
463 (_, Left ko) -> R.fail_with "transaction infer_equilibrium"
464 (Error_transaction_not_equilibrated tr_unchecked ko)
465 (_bal, Right ok) -> return ok
466 transaction_balanced_virtual_postings <-
467 case Balance.infer_equilibrium balanced_virtual_postings_unchecked of
468 (_, Left ko) -> R.fail_with "transaction infer_equilibrium"
469 (Error_virtual_transaction_not_equilibrated tr_unchecked ko)
470 (_bal, Right ok) -> return ok
473 { transaction_postings
474 , transaction_balanced_virtual_postings
481 code :: (Consable ts t, Stream s m Char) => ParsecT s (Context 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
506 default_year :: (Consable ts t, Stream s m Char) => ParsecT s (Context ts t) m ()
508 year <- R.integer_of_digits 10 <$> R.many1 R.digit
509 R.skipMany R.space_horizontal >> R.new_line
510 context_ <- R.getState
511 R.setState context_{context_year=year}
514 default_unit_and_style :: (Consable ts t, Stream s m Char) => ParsecT s (Context ts t) m ()
515 default_unit_and_style = (do
516 amount_ <- Amount.Read.amount
517 R.skipMany R.space_horizontal >> R.new_line
518 context_ <- R.getState
519 R.setState context_{context_unit_and_style =
521 ( Amount.unit amount_
522 , Amount.style amount_ )}
523 ) <?> "default unit and style"
526 ( Consable ts Transaction
527 , Show (ts Transaction)
528 , Stream s (R.Error_State Error IO) Char
530 => ParsecT s (Context ts Transaction) (R.Error_State Error IO) ()
532 sourcepos <- R.getPosition
533 filename <- R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
534 context_ <- R.getState
535 let journal_ = context_journal context_
536 let cwd = Path.takeDirectory (R.sourceName sourcepos)
537 file_path <- liftIO $ Path.abs cwd filename
539 join $ liftIO $ Exception.catch
540 (liftM return $ readFile file_path)
541 (return . R.fail_with "include reading" . Error_reading_file file_path)
542 (journal_included, context_included) <- do
544 R.runParserT_with_Error (R.and_state $ journal_rec file_path)
545 context_{context_journal = Ledger.journal}
548 Right ok -> return ok
549 Left ko -> R.fail_with "include parsing" (Error_including_file file_path ko)
551 context_included{context_journal=
552 journal_{journal_includes=
553 journal_included{journal_file=file_path}
554 : journal_includes journal_}}
560 ( Consable ts Transaction
561 , Show (ts Transaction)
562 , Stream s (R.Error_State Error IO) Char
565 -> ParsecT s (Context ts Transaction) (R.Error_State Error IO) (Journal ts Transaction)
567 currentLocalTime <- liftIO $
569 <$> Time.getCurrentTimeZone
570 <*> Time.getCurrentTime
571 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
572 context_ <- R.getState
573 R.setState $ context_{context_year=currentLocalYear}
578 ( Consable ts Transaction
579 , Show (ts Transaction)
580 , Stream s (R.Error_State Error IO) Char
583 -> ParsecT s (Context ts Transaction) (R.Error_State Error IO) (Journal ts Transaction)
584 journal_rec file_ = do
585 last_read_time <- lift $ liftIO Time.getCurrentTime
588 [ R.skipMany1 R.space
590 [ R.string "Y" >> return default_year
591 , R.string "D" >> return default_unit_and_style
592 , R.string "!include" >> return include
594 >>= \r -> R.skipMany1 R.space_horizontal >> r)
597 context_' <- R.getState
598 let j = context_journal context_'
599 R.setState $ context_'{context_journal=
600 j{journal_transactions=mcons t $ journal_transactions j}}
601 R.new_line <|> R.eof))
602 , R.try (comment >> return ())
605 journal_ <- context_journal <$> R.getState
608 { journal_file = file_
609 , journal_last_read_time = last_read_time
610 , journal_includes = reverse $ journal_includes journal_
613 -- ** Read 'Journal' from a file
617 ( Consable ts Transaction
618 , Show (ts Transaction)
620 => Context ts Transaction
622 -> ExceptT [R.Error Error] IO (Journal ts Transaction)
626 (liftM Right $ Text.IO.readFile path) $
627 \ko -> return $ Left $
628 [ R.Error_Custom (R.initialPos path) $ Error_reading_file path ko ]
629 >>= liftIO . R.runParserT_with_Error (journal path) ctx path
631 Left ko -> throwE $ ko
632 Right ok -> ExceptT $ return $ Right ok