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 Control.Arrow ((***), first)
11 import qualified Control.Exception as Exception
12 import Control.Monad (Monad(..), guard, liftM, join, forM, void)
13 import Control.Monad.IO.Class (liftIO)
14 import Control.Monad.Trans.Except (ExceptT(..), throwE)
16 import Data.Char (Char, isSpace)
17 import Data.Either (Either(..), either)
18 import Data.Eq (Eq(..))
20 import Data.List.NonEmpty (NonEmpty(..))
21 import qualified Data.List.NonEmpty as NonEmpty
22 import Data.Map.Strict (Map)
23 import qualified Data.Map.Strict as Data.Map
24 import Data.Maybe (Maybe(..), fromMaybe, maybe)
25 import Data.String (fromString)
26 import qualified Data.Text as Text
27 import qualified Data.Text.IO as Text.IO (readFile)
28 import qualified Data.Time.Calendar as Time
29 import qualified Data.Time.Clock as Time
30 import qualified Data.Time.LocalTime as Time
31 import Data.Tuple (fst, snd)
32 import Data.Typeable ()
33 import Prelude (($), (.), IO, FilePath, const, flip, id)
34 import qualified System.FilePath.Posix as Path
35 import qualified Text.Parsec as R hiding
48 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
49 import qualified Text.Parsec.Pos as R
50 import Text.Show (Show)
52 import Hcompta.Account (Account)
53 import qualified Hcompta.Account as Account
54 import qualified Hcompta.Account.Read as Account.Read
55 import qualified Hcompta.Amount as Amount
56 import qualified Hcompta.Amount.Read as Amount.Read
57 import qualified Hcompta.Amount.Style as Style
58 import qualified Hcompta.Amount.Unit as Unit
59 import qualified Hcompta.Balance as Balance
60 import Hcompta.Date (Date)
61 import qualified Hcompta.Date as Date
62 import qualified Hcompta.Date.Read as Date.Read
63 import Hcompta.Format.Ledger
69 import qualified Hcompta.Format.Ledger as Ledger
70 import Hcompta.Lib.Consable (Consable(..))
71 import qualified Hcompta.Lib.Parsec as R
72 import qualified Hcompta.Lib.Path as Path
73 import Hcompta.Lib.Regex (Regex)
74 import Hcompta.Posting as Posting
75 import Hcompta.Tag (Tag)
76 import qualified Hcompta.Tag as Tag
80 { context_account_prefix :: !(Maybe Account)
81 , context_aliases_exact :: !(Data.Map.Map Account Account)
82 , context_aliases_joker :: ![(Account.Joker, Account)]
83 , context_aliases_regex :: ![(Regex, Account)]
84 , context_date :: !Date
85 , context_filter :: !f
86 , context_journal :: !(Journal (ts t))
87 , context_unit_and_style :: !(Maybe (Amount.Unit, Amount.Style))
88 , context_year :: !Date.Year
92 :: (Show f, Consable f ts t)
93 => f -> Journal (ts t) -> Context f ts t
94 context flt context_journal =
96 { context_account_prefix = Nothing
97 , context_aliases_exact = Data.Map.empty
98 , context_aliases_joker = []
99 , context_aliases_regex = []
100 , context_date = Date.nil
101 , context_filter = flt
103 , context_unit_and_style = Nothing
104 , context_year = Date.year Date.nil
108 = Error_date Date.Read.Error
109 | Error_transaction_not_equilibrated Transaction [Balance.Unit_Sum (Balance.Posting_Amount Ledger.Posting)]
110 | Error_virtual_transaction_not_equilibrated Transaction [Balance.Unit_Sum (Balance.Posting_Amount Ledger.Posting)]
111 | Error_reading_file FilePath Exception.IOException
112 | Error_including_file FilePath [R.Error Error]
118 :: (Consable f ts t, Stream s m Char)
119 => ParsecT s (Context f ts t) m ()
121 _ <- R.string "alias"
122 R.skipMany1 $ R.space_horizontal
123 pattern <- Account.Read.pattern
124 R.skipMany $ R.space_horizontal
126 R.skipMany $ R.space_horizontal
127 repl <- Account.Read.account
128 R.skipMany $ R.space_horizontal
130 Account.Pattern_Exact acct -> R.modifyState $ \ctx -> ctx{context_aliases_exact=
131 Data.Map.insert acct repl $ context_aliases_exact ctx}
132 Account.Pattern_Joker jokr -> R.modifyState $ \ctx -> ctx{context_aliases_joker=
133 (jokr, repl):context_aliases_joker ctx}
134 Account.Pattern_Regex regx -> R.modifyState $ \ctx -> ctx{context_aliases_regex=
135 (regx, repl):context_aliases_regex ctx}
140 comment_begin :: Char
143 comment :: Stream s m Char => ParsecT s u m Comment
145 _ <- R.char comment_begin
147 R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
150 comments :: Stream s m Char => ParsecT s u m [Comment]
154 R.many1_separated comment (R.new_line >> R.skipMany R.space_horizontal)
160 tag_value_sep :: Char
166 tag_path_section_char :: Stream s m Char => ParsecT s u m Char
167 tag_path_section_char =
168 R.satisfy (\c -> c /= tag_value_sep && c /= tag_sep && not (Data.Char.isSpace c))
170 tag :: Stream s m Char => ParsecT s u m Tag
171 tag = ((,) <$> tag_path <*> tag_value) <?> "tag"
173 tag_path :: Stream s m Char => ParsecT s u m Tag.Path
175 NonEmpty.fromList <$> do
176 R.many1 $ R.try tag_path_section
178 tag_path_section :: Stream s m Char => ParsecT s u m Tag.Section
179 tag_path_section = do
181 ((R.many1 $ tag_path_section_char) <* R.char tag_value_sep)
183 tag_value :: Stream s m Char => ParsecT s u m Tag.Value
186 R.manyTill R.anyChar $ do
188 R.try (R.char tag_sep >> R.many R.space_horizontal >> void tag_path_section)
189 <|> R.try (void (R.try R.new_line))
192 tags :: Stream s m Char => ParsecT s u m (Map Tag.Path [Tag.Value])
194 Data.Map.fromListWith (flip (++))
195 . map (\(p, v) -> (p, [v])) <$> do
196 R.many_separated tag $ do
198 R.skipMany $ R.space_horizontal
200 not_tag :: Stream s m Char => ParsecT s u m ()
202 R.skipMany $ R.try $ do
203 R.skipMany $ tag_path_section_char
209 :: (Consable f ts t, Stream s (R.Error_State Error m) Char, Monad m)
210 => ParsecT s (Context f ts t) (R.Error_State Error m) (Posting, Posting.Posting_Type)
213 sourcepos <- R.getPosition
214 R.skipMany1 $ R.space_horizontal
216 R.skipMany $ R.space_horizontal
217 acct <- Account.Read.account
218 let (type_, account_) = posting_type acct
222 (void R.tab <|> void (R.count 2 R.space_horizontal))
223 R.skipMany $ R.space_horizontal
225 if u == Unit.nil then id
227 Data.Map.adjust (\a ->
228 a{ Amount.style = s{Style.precision = Style.precision $ Amount.style a}
231 (context_unit_and_style ctx) .
232 Amount.from_List <$> do
233 R.many_separated Amount.Read.amount $ do
234 R.skipMany $ R.space_horizontal
235 _ <- R.char amount_sep
236 R.skipMany $ R.space_horizontal
238 , return Data.Map.empty
240 R.skipMany $ R.space_horizontal
241 -- TODO: balance assertion
243 comments_ <- comments
244 let tags_ = tags_of_comments comments_
246 case Data.Map.lookup ("date":|[]) tags_ of
249 let date2s = Data.Map.lookup ("date2":|[]) tags_ -- NOTE: support hledger's date2
251 forM (dates ++ fromMaybe [] date2s) $ \s ->
252 R.runParserT_with_Error_fail "tag date" id
253 (Date.Read.date Error_date (Just $ context_year ctx) <* R.eof) ()
255 >>= \dates_ -> case (dates, date2s) of -- NOTE: put hledger's date2 at least in second position
257 return $ context_date ctx:dates_
260 { posting_account=account_
261 , posting_amounts=amounts_
262 , posting_comments=comments_
263 , posting_dates=dates_
264 , posting_sourcepos=sourcepos
265 , posting_status=status_
273 tags_of_comments :: [Comment] -> Map Tag.Path [Tag.Value]
275 Data.Map.unionsWith (++)
277 ( Data.Either.either (const Data.Map.empty) id
278 . R.runParser (not_tag >> tags <* R.eof) () "" )
280 status :: Stream s m Char => ParsecT s u m Ledger.Status
283 R.skipMany $ R.space_horizontal
284 _ <- (R.char '*' <|> R.char '!')
289 -- | Return the 'Posting_Type' and stripped 'Account' of the given 'Account'.
290 posting_type :: Account -> (Posting_Type, Account)
292 fromMaybe (Posting_Type_Regular, acct) $ do
295 case Text.stripPrefix virtual_begin name of
297 name'' <- liftM Text.strip $ Text.stripSuffix virtual_end name'
298 guard $ not $ Text.null name''
299 Just (Posting_Type_Virtual, name'':|[])
301 name' <- liftM Text.strip $
302 Text.stripPrefix virtual_balanced_begin name
303 >>= Text.stripSuffix virtual_balanced_end
304 guard $ not $ Text.null name'
305 Just (Posting_Type_Virtual_Balanced, name':|[])
306 first_name:|acct' -> do
307 let rev_acct' = Data.List.reverse acct'
308 let last_name = Data.List.head rev_acct'
309 case liftM Text.stripStart $
310 Text.stripPrefix virtual_begin first_name of
311 Just first_name' -> do
312 last_name' <- liftM Text.stripEnd $
313 Text.stripSuffix virtual_end last_name
314 guard $ not $ Text.null first_name'
315 guard $ not $ Text.null last_name'
317 ( Posting_Type_Virtual
319 Data.List.reverse (last_name':Data.List.tail rev_acct')
322 first_name' <- liftM Text.stripStart $
323 Text.stripPrefix virtual_balanced_begin first_name
324 last_name' <- liftM Text.stripEnd $
325 Text.stripSuffix virtual_balanced_end last_name
326 guard $ not $ Text.null first_name'
327 guard $ not $ Text.null last_name'
329 ( Posting_Type_Virtual_Balanced
331 Data.List.reverse (last_name':Data.List.tail rev_acct')
334 virtual_begin = Text.singleton posting_type_virtual_begin
335 virtual_end = Text.singleton posting_type_virtual_end
336 virtual_balanced_begin = Text.singleton posting_type_virtual_balanced_begin
337 virtual_balanced_end = Text.singleton posting_type_virtual_balanced_end
339 posting_type_virtual_begin :: Char
340 posting_type_virtual_begin = '('
341 posting_type_virtual_balanced_begin :: Char
342 posting_type_virtual_balanced_begin = '['
343 posting_type_virtual_end :: Char
344 posting_type_virtual_end = ')'
345 posting_type_virtual_balanced_end :: Char
346 posting_type_virtual_balanced_end = ']'
348 -- * Read 'Transaction'
351 :: (Consable f ts t, Stream s (R.Error_State Error m) Char, Monad m)
352 => ParsecT s (Context f ts t) (R.Error_State Error m) Transaction
355 transaction_sourcepos <- R.getPosition
356 transaction_comments_before <-
360 _ -> return x <* R.new_line
361 date_ <- Date.Read.date Error_date (Just $ context_year ctx)
363 R.option [] $ R.try $ do
364 R.skipMany $ R.space_horizontal
366 R.skipMany $ R.space_horizontal
368 (Date.Read.date Error_date (Just $ context_year ctx)) $
370 R.many $ R.space_horizontal
372 >> (R.many $ R.space_horizontal)
373 let transaction_dates = (date_, dates_)
374 R.skipMany $ R.space_horizontal
375 transaction_status <- status
376 transaction_code <- R.option "" $ R.try code
377 R.skipMany $ R.space_horizontal
378 transaction_description <- description
379 R.skipMany $ R.space_horizontal
380 transaction_comments_after <- comments
381 let transaction_tags =
382 Data.Map.unionWith (++)
383 (tags_of_comments transaction_comments_before)
384 (tags_of_comments transaction_comments_after)
386 (postings_unchecked, postings_not_regular) <-
387 first (Ledger.posting_by_Account . Data.List.map fst) .
388 Data.List.partition ((Posting.Posting_Type_Regular ==) . snd) <$>
389 R.many1_separated posting R.new_line
390 let (transaction_virtual_postings, balanced_virtual_postings_unchecked) =
391 join (***) (Ledger.posting_by_Account . Data.List.map fst) $
392 Data.List.partition ((Posting.Posting_Type_Virtual ==) . snd)
397 , transaction_comments_before
398 , transaction_comments_after
400 , transaction_description
401 , transaction_postings=postings_unchecked
402 , transaction_virtual_postings
403 , transaction_balanced_virtual_postings=balanced_virtual_postings_unchecked
404 , transaction_sourcepos
408 transaction_postings <-
409 case Balance.infer_equilibrium postings_unchecked of
410 (_, Left ko) -> R.fail_with "transaction infer_equilibrium"
411 (Error_transaction_not_equilibrated tr_unchecked ko)
412 (_bal, Right ok) -> return ok
413 transaction_balanced_virtual_postings <-
414 case Balance.infer_equilibrium balanced_virtual_postings_unchecked of
415 (_, Left ko) -> R.fail_with "transaction infer_equilibrium"
416 (Error_virtual_transaction_not_equilibrated tr_unchecked ko)
417 (_bal, Right ok) -> return ok
420 { transaction_postings
421 , transaction_balanced_virtual_postings
428 code :: (Consable f ts t, Stream s m Char)
429 => ParsecT s (Context f ts t) m Ledger.Code
432 R.skipMany $ R.space_horizontal
433 R.between (R.char '(') (R.char ')') $
434 R.many $ R.satisfy (\c -> c /= ')' && not (R.is_space_horizontal c))
437 description :: Stream s m Char => ParsecT s u m Ledger.Description
440 R.many $ R.try description_char
443 description_char :: Stream s m Char => ParsecT s u m Char
444 description_char = do
447 _ | c == comment_begin -> R.parserZero
448 _ | R.is_space_horizontal c -> return c <* (R.lookAhead $ R.try $ description_char)
449 _ | not (Data.Char.isSpace c) -> return c
455 :: (Consable f ts t, Stream s m Char)
456 => ParsecT s (Context f ts t) m ()
458 year <- R.integer_of_digits 10 <$> R.many1 R.digit
459 R.skipMany R.space_horizontal >> R.new_line
460 context_ <- R.getState
461 R.setState context_{context_year=year}
464 default_unit_and_style
465 :: (Consable f ts t, Stream s m Char)
466 => ParsecT s (Context f ts t) m ()
467 default_unit_and_style = (do
468 amount_ <- Amount.Read.amount
469 R.skipMany R.space_horizontal >> R.new_line
470 context_ <- R.getState
471 R.setState context_{context_unit_and_style =
473 ( Amount.unit amount_
474 , Amount.style amount_ )}
475 ) <?> "default unit and style"
478 ( Consable f ts Transaction
480 , Show (ts Transaction)
481 , Stream s (R.Error_State Error IO) Char
483 => ParsecT s (Context f ts Transaction) (R.Error_State Error IO) ()
485 sourcepos <- R.getPosition
486 filename <- R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
487 context_ <- R.getState
488 let journal_ = context_journal context_
489 let cwd = Path.takeDirectory (R.sourceName sourcepos)
490 file_path <- liftIO $ Path.abs cwd filename
492 join $ liftIO $ Exception.catch
493 (liftM return $ Text.IO.readFile file_path)
494 (return . R.fail_with "include reading" . Error_reading_file file_path)
495 (journal_included, context_included) <- do
497 R.runParserT_with_Error (R.and_state $ journal_rec file_path)
498 context_{context_journal = Ledger.journal}
501 Right ok -> return ok
502 Left ko -> R.fail_with "include parsing" (Error_including_file file_path ko)
504 context_included{context_journal=
505 journal_{journal_includes=
506 journal_included{journal_file=file_path}
507 : journal_includes journal_}}
513 ( Consable f ts Transaction
515 , Show (ts Transaction)
516 , Stream s (R.Error_State Error IO) Char
519 -> ParsecT s (Context f ts Transaction) (R.Error_State Error IO) (Journal (ts Transaction))
521 currentLocalTime <- liftIO $
523 <$> Time.getCurrentTimeZone
524 <*> Time.getCurrentTime
525 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
527 R.setState $ ctx{context_year=currentLocalYear}
532 ( Consable f ts Transaction
534 , Show (ts Transaction)
535 , Stream s (R.Error_State Error IO) Char
538 -> ParsecT s (Context f ts Transaction) (R.Error_State Error IO) (Journal (ts Transaction))
539 journal_rec file_ = do
540 last_read_time <- liftIO Date.now
543 [ R.skipMany1 R.space
545 [ R.string "Y" >> return default_year
546 , R.string "D" >> return default_unit_and_style
547 , R.string "!include" >> return include
549 >>= \r -> R.skipMany1 R.space_horizontal >> r)
553 let j = context_journal ctx
556 j{journal_transactions=
557 mcons (context_filter ctx) t $
558 journal_transactions j}}
559 R.new_line <|> R.eof))
560 , R.try (void $ comment)
563 journal_ <- context_journal <$> R.getState
566 { journal_file = file_
567 , journal_last_read_time = last_read_time
568 , journal_includes = reverse $ journal_includes journal_
571 -- ** Read 'Journal' from a file
575 ( Consable f ts Transaction
577 , Show (ts Transaction)
579 => Context f ts Transaction
581 -> ExceptT [R.Error Error] IO (Journal (ts Transaction))
585 (liftM Right $ Text.IO.readFile path) $
586 \ko -> return $ Left $
587 [ R.Error_Custom (R.initialPos path) $ Error_reading_file path ko ]
588 >>= liftIO . R.runParserT_with_Error (journal path) ctx path
590 Left ko -> throwE $ ko
591 Right ok -> ExceptT $ return $ Right ok