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 (guard, liftM, join, 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.Text as Text
23 import qualified Data.Time.Calendar as Time
24 import qualified Data.Time.Clock as Time
25 import qualified Data.Time.LocalTime as Time
26 import Data.Typeable ()
27 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 System.FilePath.Posix as Path
45 import qualified Hcompta.Account as Account
46 import Hcompta.Account (Account)
47 import qualified Hcompta.Account.Read as Account.Read
48 import qualified Hcompta.Balance as Balance
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.Posting as Posting
58 import Hcompta.Format.Ledger
62 , Tag, Tag_Name, Tag_Value, Tag_by_Name
65 import Hcompta.Lib.Consable (Consable(..))
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_filter :: !f
78 , context_journal :: !(Journal (ts t))
79 , context_unit_and_style :: !(Maybe (Amount.Unit, Amount.Style))
80 , context_year :: !Date.Year
84 :: (Show f, Consable f ts t)
85 => f -> Journal (ts t) -> Context f ts t
86 context flt context_journal =
88 { context_account_prefix = Nothing
89 , context_aliases_exact = Data.Map.empty
90 , context_aliases_joker = []
91 , context_aliases_regex = []
92 , context_date = Date.nil
93 , context_filter = flt
95 , context_unit_and_style = Nothing
96 , context_year = Date.year Date.nil
100 = Error_date Date.Read.Error
101 | Error_transaction_not_equilibrated Transaction [Balance.Unit_Sum (Balance.Posting_Amount Ledger.Posting)]
102 | Error_virtual_transaction_not_equilibrated Transaction [Balance.Unit_Sum (Balance.Posting_Amount Ledger.Posting)]
103 | Error_reading_file FilePath Exception.IOException
104 | Error_including_file FilePath [R.Error Error]
110 :: (Consable f ts t, Stream s m Char)
111 => ParsecT s (Context f ts t) m ()
113 _ <- R.string "alias"
114 R.skipMany1 $ R.space_horizontal
115 pattern <- Account.Read.pattern
116 R.skipMany $ R.space_horizontal
118 R.skipMany $ R.space_horizontal
119 repl <- Account.Read.account
120 R.skipMany $ R.space_horizontal
122 Account.Pattern_Exact acct -> R.modifyState $ \ctx -> ctx{context_aliases_exact=
123 Data.Map.insert acct repl $ context_aliases_exact ctx}
124 Account.Pattern_Joker jokr -> R.modifyState $ \ctx -> ctx{context_aliases_joker=
125 (jokr, repl):context_aliases_joker ctx}
126 Account.Pattern_Regex regx -> R.modifyState $ \ctx -> ctx{context_aliases_regex=
127 (regx, repl):context_aliases_regex ctx}
132 comment_begin :: Char
135 comment :: Stream s m Char => ParsecT s u m Comment
137 _ <- R.char comment_begin
139 R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
142 comments :: Stream s m Char => ParsecT s u m [Comment]
146 R.many1_separated comment (R.new_line >> R.skipMany R.space_horizontal)
152 tag_value_sep :: Char
159 tag :: Stream s m Char => ParsecT s u m Tag
162 _ <- R.char tag_value_sep
167 tag_name :: Stream s m Char => ParsecT s u m Tag_Name
170 R.many1 $ R.satisfy (\c -> c /= tag_value_sep && not (Data.Char.isSpace c))
172 tag_value :: Stream s m Char => ParsecT s u m Tag_Value
175 R.manyTill R.anyChar $ do
177 R.try (R.char tag_sep >> R.many R.space_horizontal >> tag_name >> void (R.char tag_value_sep))
181 tags :: Stream s m Char => ParsecT s u m Tag_by_Name
183 Ledger.tag_by_Name <$> do
184 R.many_separated tag $ do
186 R.skipMany $ R.space_horizontal
189 not_tag :: Stream s m Char => ParsecT s u m ()
191 R.skipMany $ R.try $ do
192 R.skipMany $ R.satisfy
193 (\c -> c /= tag_value_sep
194 && not (Data.Char.isSpace c))
200 :: (Consable f ts t, Stream s (R.Error_State Error m) Char, Monad m)
201 => ParsecT s (Context f ts t) (R.Error_State Error m) (Posting, Posting.Posting_Type)
204 sourcepos <- R.getPosition
205 R.skipMany1 $ R.space_horizontal
207 R.skipMany $ R.space_horizontal
208 acct <- Account.Read.account
209 let (type_, account_) = posting_type acct
213 (void R.tab <|> void (R.count 2 R.space_horizontal))
214 R.skipMany $ R.space_horizontal
216 if u == Unit.nil then id
218 Data.Map.adjust (\a ->
219 a{ Amount.style = s{Style.precision = Style.precision $ Amount.style a}
222 (context_unit_and_style ctx) .
223 Amount.from_List <$> do
224 R.many_separated Amount.Read.amount $ do
225 R.skipMany $ R.space_horizontal
226 _ <- R.char amount_sep
227 R.skipMany $ R.space_horizontal
229 , return Data.Map.empty
231 R.skipMany $ R.space_horizontal
232 -- TODO: balance assertion
234 comments_ <- comments
235 let tags_ = tags_of_comments comments_
237 case Data.Map.lookup "date" tags_ of
240 let date2s = Data.Map.lookup "date2" tags_ -- NOTE: support hledger's date2
242 forM (dates ++ fromMaybe [] date2s) $ \s ->
243 R.runParserT_with_Error_fail "tag date" id
244 (Date.Read.date Error_date (Just $ context_year ctx) <* R.eof) ()
246 >>= \dates_ -> case (dates, date2s) of -- NOTE: put hledger's date2 at least in second position
248 return $ context_date ctx:dates_
251 { posting_account=account_
252 , posting_amounts=amounts_
253 , posting_comments=comments_
254 , posting_dates=dates_
255 , posting_sourcepos=sourcepos
256 , posting_status=status_
264 tags_of_comments :: [Comment] -> Tag_by_Name
266 Data.Map.unionsWith (++)
268 ( Data.Either.either (const Data.Map.empty) id
269 . R.runParser (not_tag >> tags <* R.eof) () "" )
271 status :: Stream s m Char => ParsecT s u m Ledger.Status
274 R.skipMany $ R.space_horizontal
275 _ <- (R.char '*' <|> R.char '!')
280 -- | Return the 'Posting_Type' and stripped 'Account' of the given 'Account'.
281 posting_type :: Account -> (Posting_Type, Account)
283 fromMaybe (Posting_Type_Regular, acct) $ do
286 case Text.stripPrefix virtual_begin name of
288 name'' <- liftM Text.strip $ Text.stripSuffix virtual_end name'
289 guard $ not $ Text.null name''
290 Just (Posting_Type_Virtual, name'':|[])
292 name' <- liftM Text.strip $
293 Text.stripPrefix virtual_balanced_begin name
294 >>= Text.stripSuffix virtual_balanced_end
295 guard $ not $ Text.null name'
296 Just (Posting_Type_Virtual_Balanced, name':|[])
297 first_name:|acct' -> do
298 let rev_acct' = Data.List.reverse acct'
299 let last_name = Data.List.head rev_acct'
300 case liftM Text.stripStart $
301 Text.stripPrefix virtual_begin first_name of
302 Just first_name' -> do
303 last_name' <- liftM Text.stripEnd $
304 Text.stripSuffix virtual_end last_name
305 guard $ not $ Text.null first_name'
306 guard $ not $ Text.null last_name'
308 ( Posting_Type_Virtual
310 Data.List.reverse (last_name':Data.List.tail rev_acct')
313 first_name' <- liftM Text.stripStart $
314 Text.stripPrefix virtual_balanced_begin first_name
315 last_name' <- liftM Text.stripEnd $
316 Text.stripSuffix virtual_balanced_end last_name
317 guard $ not $ Text.null first_name'
318 guard $ not $ Text.null last_name'
320 ( Posting_Type_Virtual_Balanced
322 Data.List.reverse (last_name':Data.List.tail rev_acct')
325 virtual_begin = Text.singleton posting_type_virtual_begin
326 virtual_end = Text.singleton posting_type_virtual_end
327 virtual_balanced_begin = Text.singleton posting_type_virtual_balanced_begin
328 virtual_balanced_end = Text.singleton posting_type_virtual_balanced_end
330 posting_type_virtual_begin :: Char
331 posting_type_virtual_begin = '('
332 posting_type_virtual_balanced_begin :: Char
333 posting_type_virtual_balanced_begin = '['
334 posting_type_virtual_end :: Char
335 posting_type_virtual_end = ')'
336 posting_type_virtual_balanced_end :: Char
337 posting_type_virtual_balanced_end = ']'
339 -- * Read 'Transaction'
342 :: (Consable f ts t, Stream s (R.Error_State Error m) Char, Monad m)
343 => ParsecT s (Context f ts t) (R.Error_State Error m) Transaction
346 transaction_sourcepos <- R.getPosition
347 transaction_comments_before <-
351 _ -> return x <* R.new_line
352 date_ <- Date.Read.date Error_date (Just $ context_year ctx)
354 R.option [] $ R.try $ do
355 R.skipMany $ R.space_horizontal
357 R.skipMany $ R.space_horizontal
359 (Date.Read.date Error_date (Just $ context_year ctx)) $
361 R.many $ R.space_horizontal
363 >> (R.many $ R.space_horizontal)
364 let transaction_dates = (date_, dates_)
365 R.skipMany $ R.space_horizontal
366 transaction_status <- status
367 transaction_code <- R.option "" $ R.try code
368 R.skipMany $ R.space_horizontal
369 transaction_description <- description
370 R.skipMany $ R.space_horizontal
371 transaction_comments_after <- comments
372 let transaction_tags =
373 Data.Map.unionWith (++)
374 (tags_of_comments transaction_comments_before)
375 (tags_of_comments transaction_comments_after)
377 (postings_unchecked, postings_not_regular) <-
378 first (Ledger.posting_by_Account . Data.List.map fst) .
379 Data.List.partition ((Posting.Posting_Type_Regular ==) . snd) <$>
380 R.many1_separated posting R.new_line
381 let (transaction_virtual_postings, balanced_virtual_postings_unchecked) =
382 join (***) (Ledger.posting_by_Account . Data.List.map fst) $
383 Data.List.partition ((Posting.Posting_Type_Virtual ==) . snd)
388 , transaction_comments_before
389 , transaction_comments_after
391 , transaction_description
392 , transaction_postings=postings_unchecked
393 , transaction_virtual_postings
394 , transaction_balanced_virtual_postings=balanced_virtual_postings_unchecked
395 , transaction_sourcepos
399 transaction_postings <-
400 case Balance.infer_equilibrium postings_unchecked of
401 (_, Left ko) -> R.fail_with "transaction infer_equilibrium"
402 (Error_transaction_not_equilibrated tr_unchecked ko)
403 (_bal, Right ok) -> return ok
404 transaction_balanced_virtual_postings <-
405 case Balance.infer_equilibrium balanced_virtual_postings_unchecked of
406 (_, Left ko) -> R.fail_with "transaction infer_equilibrium"
407 (Error_virtual_transaction_not_equilibrated tr_unchecked ko)
408 (_bal, Right ok) -> return ok
411 { transaction_postings
412 , transaction_balanced_virtual_postings
419 code :: (Consable f ts t, Stream s m Char)
420 => ParsecT s (Context f ts t) m Ledger.Code
423 R.skipMany $ R.space_horizontal
424 R.between (R.char '(') (R.char ')') $
425 R.many $ R.satisfy (\c -> c /= ')' && not (R.is_space_horizontal c))
428 description :: Stream s m Char => ParsecT s u m Ledger.Description
431 R.many $ R.try description_char
434 description_char :: Stream s m Char => ParsecT s u m Char
435 description_char = do
438 _ | c == comment_begin -> R.parserZero
439 _ | R.is_space_horizontal c -> return c <* (R.lookAhead $ R.try $ description_char)
440 _ | not (Data.Char.isSpace c) -> return c
446 :: (Consable f ts t, Stream s m Char)
447 => ParsecT s (Context f ts t) m ()
449 year <- R.integer_of_digits 10 <$> R.many1 R.digit
450 R.skipMany R.space_horizontal >> R.new_line
451 context_ <- R.getState
452 R.setState context_{context_year=year}
455 default_unit_and_style
456 :: (Consable f ts t, Stream s m Char)
457 => ParsecT s (Context f ts t) m ()
458 default_unit_and_style = (do
459 amount_ <- Amount.Read.amount
460 R.skipMany R.space_horizontal >> R.new_line
461 context_ <- R.getState
462 R.setState context_{context_unit_and_style =
464 ( Amount.unit amount_
465 , Amount.style amount_ )}
466 ) <?> "default unit and style"
469 ( Consable f ts Transaction
471 , Show (ts Transaction)
472 , Stream s (R.Error_State Error IO) Char
474 => ParsecT s (Context f ts Transaction) (R.Error_State Error IO) ()
476 sourcepos <- R.getPosition
477 filename <- R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
478 context_ <- R.getState
479 let journal_ = context_journal context_
480 let cwd = Path.takeDirectory (R.sourceName sourcepos)
481 file_path <- liftIO $ Path.abs cwd filename
483 join $ liftIO $ Exception.catch
484 (liftM return $ readFile file_path)
485 (return . R.fail_with "include reading" . Error_reading_file file_path)
486 (journal_included, context_included) <- do
488 R.runParserT_with_Error (R.and_state $ journal_rec file_path)
489 context_{context_journal = Ledger.journal}
492 Right ok -> return ok
493 Left ko -> R.fail_with "include parsing" (Error_including_file file_path ko)
495 context_included{context_journal=
496 journal_{journal_includes=
497 journal_included{journal_file=file_path}
498 : journal_includes journal_}}
504 ( Consable f ts Transaction
506 , Show (ts Transaction)
507 , Stream s (R.Error_State Error IO) Char
510 -> ParsecT s (Context f ts Transaction) (R.Error_State Error IO) (Journal (ts Transaction))
512 currentLocalTime <- liftIO $
514 <$> Time.getCurrentTimeZone
515 <*> Time.getCurrentTime
516 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
518 R.setState $ ctx{context_year=currentLocalYear}
523 ( Consable f ts Transaction
525 , Show (ts Transaction)
526 , Stream s (R.Error_State Error IO) Char
529 -> ParsecT s (Context f ts Transaction) (R.Error_State Error IO) (Journal (ts Transaction))
530 journal_rec file_ = do
531 last_read_time <- liftIO Date.now
534 [ R.skipMany1 R.space
536 [ R.string "Y" >> return default_year
537 , R.string "D" >> return default_unit_and_style
538 , R.string "!include" >> return include
540 >>= \r -> R.skipMany1 R.space_horizontal >> r)
544 let j = context_journal ctx
547 j{journal_transactions=
548 mcons (context_filter ctx) t $
549 journal_transactions j}}
550 R.new_line <|> R.eof))
551 , R.try (void $ comment)
554 journal_ <- context_journal <$> R.getState
557 { journal_file = file_
558 , journal_last_read_time = last_read_time
559 , journal_includes = reverse $ journal_includes journal_
562 -- ** Read 'Journal' from a file
566 ( Consable f ts Transaction
568 , Show (ts Transaction)
570 => Context f ts Transaction
572 -> ExceptT [R.Error Error] IO (Journal (ts Transaction))
576 (liftM Right $ Text.IO.readFile path) $
577 \ko -> return $ Left $
578 [ R.Error_Custom (R.initialPos path) $ Error_reading_file path ko ]
579 >>= liftIO . R.runParserT_with_Error (journal path) ctx path
581 Left ko -> throwE $ ko
582 Right ok -> ExceptT $ return $ Right ok