1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE NamedFieldPuns #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE RankNTypes #-}
6 {-# LANGUAGE ScopedTypeVariables #-}
7 {-# LANGUAGE StandaloneDeriving #-}
8 {-# LANGUAGE TupleSections #-}
9 {-# LANGUAGE TypeFamilies #-}
10 module Hcompta.Format.JCC.Read where
12 import Control.Applicative ((<$>), (<*>), (<*))
13 -- import Control.Arrow ((***), first)
14 import qualified Control.Exception as Exception
15 import Control.Monad (Monad(..), liftM, join)
16 import Control.Monad.IO.Class (liftIO)
17 import Control.Monad.Trans.Except (ExceptT(..), throwE)
19 import Data.Char (Char)
20 import qualified Data.Char
21 import Data.Either (Either(..))
22 import Data.Eq (Eq(..))
23 import qualified Data.List as List
24 import Data.Semigroup as Semigroup
25 import Data.List.NonEmpty (NonEmpty(..))
26 import qualified Data.List.NonEmpty as NonEmpty
27 import Data.Map.Strict (Map)
28 import qualified Data.Map.Strict as Map
29 import Data.Maybe (Maybe(..), maybe)
30 import Data.String (fromString)
31 import Data.Text (Text)
32 import qualified Data.Text.IO as Text.IO (readFile)
33 import qualified Data.Time.Calendar as Time
34 import qualified Data.Time.Clock as Time
35 import qualified Data.Time.LocalTime as Time
36 import Data.Typeable ()
37 import Prelude (($), (.), IO, FilePath, const, flip, id)
38 import qualified System.FilePath.Posix as Path
39 import qualified Text.Parsec as R hiding
52 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
53 import qualified Text.Parsec.Pos as R
54 import Text.Show (Show)
56 import Hcompta.Account ( Account_Anchor
59 import qualified Hcompta.Account as Account
60 import qualified Hcompta.Amount as Amount
61 import Hcompta.Anchor (Anchors(..))
62 import Hcompta.Tag (Tags(..))
63 import qualified Hcompta.Balance as Balance
64 import qualified Hcompta.Chart as Chart
65 import Hcompta.Chart (Chart(..))
66 import Hcompta.Date (Date)
67 import qualified Hcompta.Date as Date
68 import qualified Hcompta.Filter.Date.Read as Date.Read
69 import Hcompta.Format.JCC
77 import qualified Hcompta.Format.JCC as JCC
78 import qualified Hcompta.Format.JCC.Amount as JCC.Amount
79 import qualified Hcompta.Format.JCC.Amount.Read as JCC.Amount.Read
80 import qualified Hcompta.Format.JCC.Amount.Style as JCC.Amount.Style
81 import Hcompta.Format.JCC.Common.Read
82 -- import qualified Hcompta.Format.JCC.Quantity as JCC.Quantity
83 import Hcompta.Lib.Consable (Consable(..))
84 import qualified Hcompta.Lib.Parsec as R
85 import qualified Hcompta.Lib.Path as Path
86 -- import Hcompta.Lib.Regex (Regex)
87 import qualified Hcompta.Lib.TreeMap as TreeMap
88 import qualified Hcompta.Polarize as Polarize
89 import Hcompta.Posting ( Posting_Anchor(..)
93 import qualified Hcompta.Posting as Posting
94 import qualified Hcompta.Quantity as Quantity
95 import Hcompta.Transaction ( Transaction_Anchor(..)
96 , Transaction_Anchors(..)
98 , Transaction_Tags(..) )
99 import qualified Hcompta.Transaction as Transaction
100 import qualified Hcompta.Unit as Unit
106 { context_cons :: Charted Transaction -> c
107 , context_date :: !Date
108 , context_journal :: !(Journal j)
109 , context_unit :: !(Maybe JCC.Unit)
110 , context_year :: !Date.Year
115 => (Charted Transaction -> c)
118 context context_cons context_journal =
121 , context_date = Date.nil
123 , context_unit = Nothing
124 , context_year = Date.year Date.nil
130 = Error_account_anchor_unknown R.SourcePos Account_Anchor
131 | Error_account_anchor_not_unique R.SourcePos Account_Anchor
132 | Error_date Date.Read.Error
133 | Error_including_file FilePath [R.Error Error]
134 | Error_reading_file FilePath Exception.IOException
135 | Error_transaction_not_equilibrated
139 , Balance.Unit_Sum Account
140 (Polarize.Polarized JCC.Quantity)
146 comment_begin :: Char
149 comment :: Stream s m Char => ParsecT s u m Comment
150 comment = (R.char comment_begin >> line) <?> "comment"
152 comments :: Stream s m Char => ParsecT s u m [Comment]
156 R.many1_separated comment (eol >> hspaces)
162 account :: Stream s m Char => ParsecT s u m JCC.Account
164 Account.from_List <$> do
165 R.many1 (R.char account_section_sep >> account_section)
167 account_section :: Stream s m Char => ParsecT s u m Text
168 account_section = name
170 account_section_sep :: Char
171 account_section_sep = '/'
173 -- ** Read 'Account_Tag'
174 account_tag_prefix :: Char
175 account_tag_prefix = '.'
176 account_tag_sep :: Char
177 account_tag_sep = ':'
178 account_tag_value_prefix :: Char
179 account_tag_value_prefix = '='
181 account_tag :: Stream s m Char => ParsecT s u m Account_Tag
183 _ <- R.char account_tag_prefix
187 R.many (R.char account_tag_sep >> name)
190 (hspaces >> R.char transaction_tag_value_prefix >> hspaces >>
191 (List.concat <$> R.many (R.choice
192 [ R.string [account_tag_prefix , account_tag_prefix] >> return [account_tag_prefix]
193 , R.string [account_anchor_prefix, account_anchor_prefix] >> return [account_anchor_prefix]
194 , (\s c -> mappend s [c])
197 c /= account_tag_prefix
198 && c /= account_anchor_prefix
203 -- ** Read 'Account_Anchor'
204 account_anchor_prefix :: Char
205 account_anchor_prefix = '~'
206 account_anchor_sep :: Char
207 account_anchor_sep = ':'
209 account_anchor :: Stream s m Char => ParsecT s u m Account_Anchor
211 _ <- R.char account_anchor_prefix
213 ps <- R.many (R.char account_anchor_sep >> name)
214 return $ Account.anchor (p:|ps)
215 ) <?> "account_anchor"
217 -- ** Read 'Account' 'Comment'
218 account_comment :: Stream s m Char => ParsecT s u m Comment
219 account_comment = comment
226 , Stream s (R.Error_State Error m) Char
227 ) => ParsecT s (Context c j)
228 (R.Error_State Error m)
231 posting_sourcepos <- R.getPosition
233 (posting_account, posting_account_anchor) <-
235 [ (,Nothing) <$> account
237 anchor <- account_anchor
239 let anchors = chart_anchors $ journal_chart $ context_journal ctx
240 case Map.lookup anchor anchors of
242 sa <- R.option Nothing $ Just <$> account
243 return $ ( a:|mappend as (maybe [] NonEmpty.toList sa)
244 , Just (anchor, sa) )
245 Nothing -> R.fail_with "account anchor"
246 (Error_account_anchor_unknown posting_sourcepos anchor)
247 ] <?> "posting_account"
253 R.many_separated JCC.Amount.Read.amount $ do
255 _ <- R.char amount_sep
257 ctx <- flip liftM R.getState $ \ctx ->
260 let jnl = context_journal ctx in
262 { JCC.journal_amount_styles =
264 (\(JCC.Amount.Style.Styles styles) (style, amt) ->
265 JCC.Amount.Style.Styles $
266 Map.insertWith mappend
267 (Amount.amount_unit amt)
269 (JCC.journal_amount_styles jnl)
275 Map.fromListWith Quantity.quantity_add $
278 let unit = Amount.amount_unit amt in
279 ( if unit == Unit.unit_empty
280 then maybe unit id (context_unit ctx)
282 , Amount.amount_quantity amt
287 ] <?> "posting_amounts"
288 (posting_tags, posting_anchors, posting_comments) <-
289 fields mempty mempty mempty
292 , posting_account_anchor
294 , posting_anchors = Posting_Anchors posting_anchors
295 , posting_tags = Posting_Tags posting_tags
303 fields :: Tags -> Anchors -> Comments
304 -> ParsecT s (Context c j)
305 (R.Error_State Error m)
306 (Tags, Anchors, Comments)
310 anchors@(Anchors anchs)
313 [ hspaces1 >> posting_comment >>= \c ->
314 fields tags anchors (c:cmts)
315 , hspaces1 >> posting_tag >>= \(Posting_Tag (p, v)) ->
316 fields (Tags $ Map.insertWith mappend p [v] tagm) anchors cmts
317 , hspaces1 >> posting_anchor >>= \(Posting_Anchor p) ->
318 fields tags (Anchors $ Map.insert p () anchs) cmts
320 fields tags anchors cmts
321 , return (tags, anchors, cmts)
327 posting_comment :: Stream s m Char => ParsecT s u m Comment
328 posting_comment = comment
330 -- ** Read 'Posting_Tag'
331 posting_tag :: Stream s m Char => ParsecT s u m Posting_Tag
332 posting_tag = (liftM (\(Transaction_Tag tag) -> Posting_Tag tag) transaction_tag) <?> "posting_tag"
334 -- ** Read 'Posting_Anchor'
335 posting_anchor :: Stream s m Char => ParsecT s u m Posting_Anchor
337 _ <- R.char transaction_anchor_prefix
339 NonEmpty.fromList <$>
340 R.many1 (R.char transaction_anchor_sep >> name)
341 ) <?> "posting_anchor"
343 -- * Read 'Transaction'
345 map_Postings_by_Account :: [Posting] -> Map Account [Posting]
346 map_Postings_by_Account =
347 Map.fromListWith (flip mappend) .
348 List.map (\p -> (posting_account p, [p]))
353 , Stream s (R.Error_State Error m) Char
354 ) => ParsecT s (Context c j) (R.Error_State Error m) Transaction
357 transaction_sourcepos <- R.getPosition
358 date_ <- Date.Read.date Error_date (Just $ context_year ctx)
360 R.option [] $ R.try $ do
365 (Date.Read.date Error_date (Just $ context_year ctx)) $
370 let transaction_dates = (date_, dates_)
372 transaction_wording <- wording
374 (transaction_tags, transaction_anchors, transaction_comments) <-
375 -- return (mempty, mempty, mempty)
376 fields mempty mempty mempty
377 transaction_postings_unchecked <-
378 map_Postings_by_Account <$> postings
379 let transaction_unchecked =
381 { transaction_anchors = Transaction_Anchors transaction_anchors
382 , transaction_tags = Transaction_Tags transaction_tags
383 , transaction_comments
385 , transaction_wording
386 , transaction_postings = transaction_postings_unchecked
387 , transaction_sourcepos
389 let styles = JCC.journal_amount_styles $ context_journal ctx
390 transaction_postings <-
391 case Balance.infer_equilibrium transaction_postings_unchecked of
392 (_, Left ko) -> R.fail_with "transaction infer_equilibrium" $
393 Error_transaction_not_equilibrated styles transaction_unchecked ko
394 (_bal, Right ok) -> return ok
396 transaction_unchecked
397 { transaction_postings
403 anchors@(Anchors anchs)
406 [ hspaces1 >> transaction_comment >>= \c ->
407 fields tags anchors (c:cmts)
408 , hspaces1 >> transaction_tag >>= \(Transaction_Tag (p, v)) ->
409 fields (Tags $ Map.insertWith mappend p [v] tagm) anchors cmts
410 , hspaces1 >> transaction_anchor >>= \(Transaction_Anchor p) ->
411 fields tags (Anchors $ Map.insert p () anchs) cmts
413 fields tags anchors cmts
414 , return (tags, anchors, cmts)
418 (Consable c j, Monad m, Stream s (R.Error_State Error m) Char)
419 => ParsecT s (Context c j) (R.Error_State Error m) [Posting]
422 (hspaces1 >> posting)
427 code :: ( Consable c j
429 => ParsecT s (Context c j) m JCC.Code
433 R.between (R.char '(') (R.char ')') $
434 R.many $ R.satisfy (\c -> c /= ')' && not (R.is_space_horizontal c))
437 wording :: Stream s m Char => ParsecT s u m JCC.Wording
440 R.many $ R.try wording_char
443 wording_char :: Stream s m Char => ParsecT s u m Char
447 _ | c == comment_begin -> R.parserZero
448 _ | R.is_space_horizontal c -> return c <* (R.lookAhead $ R.try $ wording_char)
449 _ | not (Data.Char.isSpace c) -> return c
452 -- ** Read 'Transaction_Anchor'
454 transaction_anchor_prefix :: Char
455 transaction_anchor_prefix = '@'
456 transaction_anchor_sep :: Char
457 transaction_anchor_sep = ':'
459 transaction_anchor :: Stream s m Char => ParsecT s u m Transaction_Anchor
460 transaction_anchor = (do
461 _ <- R.char transaction_anchor_prefix
463 Transaction.anchor <$>
465 R.many (R.char transaction_anchor_sep >> name)
466 ) <?> "transaction_anchor"
468 -- ** Read 'Transaction_Tag'
469 transaction_tag_prefix :: Char
470 transaction_tag_prefix = '#'
471 transaction_tag_sep :: Char
472 transaction_tag_sep = ':'
473 transaction_tag_value_prefix :: Char
474 transaction_tag_value_prefix = '='
476 transaction_tag :: Stream s m Char => ParsecT s u m Transaction_Tag
477 transaction_tag = (do
478 _ <- R.char transaction_tag_prefix
483 R.many (R.char transaction_tag_sep >> name)
486 (hspaces >> R.char transaction_tag_value_prefix >> hspaces >>
489 [ R.string [transaction_tag_prefix, transaction_tag_prefix] >>
490 return [transaction_tag_prefix]
491 , R.string [transaction_anchor_prefix, transaction_anchor_prefix] >>
492 return [transaction_anchor_prefix]
493 , (\s c -> mappend s [c])
495 <*> R.satisfy (\c -> c /= transaction_tag_prefix && c /= transaction_anchor_prefix && is_char c)
497 ) <?> "transaction_tag"
499 -- ** Read 'Transaction' 'Comment'
500 transaction_comment :: Stream s m Char => ParsecT s u m Comment
501 transaction_comment = comment
508 => ParsecT s (Context c j) m ()
510 year <- R.integer_of_digits 10 <$> R.many1 R.digit
512 context_ <- R.getState
513 R.setState context_{context_year=year}
516 default_unit_and_style
519 => ParsecT s (Context c j) m ()
520 default_unit_and_style = (do
521 (sty, amt) <- JCC.Amount.Read.amount
524 let unit = Amount.amount_unit amt
527 let jnl = context_journal ctx in
529 { JCC.journal_amount_styles =
530 let JCC.Amount.Style.Styles styles =
531 JCC.journal_amount_styles jnl in
532 JCC.Amount.Style.Styles $
533 Map.insertWith const unit sty styles
535 , context_unit = Just unit
537 ) <?> "default unit and style"
539 -- * Read included 'Journal'
544 , Stream s (R.Error_State Error IO) Char
546 => ParsecT s (Context c j)
547 (R.Error_State Error IO)
550 sourcepos <- R.getPosition
551 filename <- R.manyTill R.anyChar (R.lookAhead (R.try R.new_line <|> R.eof))
552 context_including <- R.getState
553 let journal_including = context_journal context_including
554 let cwd = Path.takeDirectory (R.sourceName sourcepos)
555 file_path <- liftIO $ Path.abs cwd filename
557 join $ liftIO $ Exception.catch
558 (liftM return $ Text.IO.readFile file_path)
559 (return . R.fail_with "include reading" . Error_reading_file file_path)
560 (journal_included, context_included) <- do
562 R.runParserT_with_Error
563 (R.and_state $ journal_rec file_path)
568 journal_chart journal_including
569 , journal_amount_styles=
570 journal_amount_styles journal_including
575 Right ok -> return ok
576 Left ko -> R.fail_with "include parsing" (Error_including_file file_path ko)
582 journal_included{journal_file=file_path} :
583 journal_includes journal_including
585 journal_chart journal_included
586 , journal_amount_styles=
587 journal_amount_styles journal_included
596 , Stream s (R.Error_State Error IO) Char
598 => ParsecT s (Context c j)
599 (R.Error_State Error IO)
602 -- sourcepos <- R.getPosition
607 , _chart_comments ) <-
608 fields acct mempty mempty mempty
610 TreeMap.singleton acct $
611 Account_Tags chart_tags
613 let j = context_journal ctx
620 { Chart.chart_accounts
621 -- , Chart.chart_tags
622 , Chart.chart_anchors
634 [ hspaces1 >> account_comment >>= \c ->
635 fields acct tags anchors (c:cmts)
636 , hspaces1 >> account_tag >>= \(Account_Tag (p, v)) ->
637 fields acct (Tags $ Map.insertWith mappend p [v] tagm) anchors cmts
638 , hspaces1 >> account_anchor >>= \anchor ->
639 case Map.insertLookupWithKey (\_k n _o -> n) anchor acct anchors of
640 (Nothing, m) -> fields acct tags m cmts
642 sourcepos <- R.getPosition
643 R.fail_with "account anchor not unique"
644 (Error_account_anchor_not_unique sourcepos anchor)
646 fields acct tags anchors cmts
647 , return (tags, anchors, cmts)
655 , Stream s (R.Error_State Error IO) Char
658 -> ParsecT s (Context c j)
659 (R.Error_State Error IO)
662 currentLocalTime <- liftIO $
664 <$> Time.getCurrentTimeZone
665 <*> Time.getCurrentTime
666 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
668 R.setState $ ctx{context_year=currentLocalYear}
675 , Stream s (R.Error_State Error IO) Char
678 -> ParsecT s (Context c j)
679 (R.Error_State Error IO)
681 journal_rec file_ = do
682 last_read_time <- liftIO Date.now
690 journal_ <- context_journal <$> R.getState
693 { journal_file = file_
694 , journal_last_read_time = last_read_time
695 , journal_includes = List.reverse $ journal_includes journal_
700 => ParsecT s u m (ParsecT s u m ())
703 R.skipMany (hspaces >> R.new_line)
705 R.skipMany (hspaces >> R.new_line)
706 R.try (hspaces >> R.eof) <|> loop r
711 , m ~ R.Error_State Error IO
713 => ParsecT s u m (ParsecT s u m ())
716 _ <- R.lookAhead (R.try $ R.char comment_begin)
720 R.modifyState $ \ctx ->
721 let j = context_journal ctx in
724 mcons (context_filter ctx) cmts $
733 , m ~ R.Error_State Error IO
735 => ParsecT s u m (ParsecT s u m ())
737 let choice s = R.string s >> hspaces1
739 [ choice "Y" >> return default_year
740 , choice "D" >> return default_unit_and_style
741 , choice "!include" >> return include
747 , m ~ R.Error_State Error IO
749 => ParsecT s u m (ParsecT s u m ())
750 jump_transaction = do
751 _ <- R.lookAhead $ R.try (R.many1 R.digit >> R.char Date.Read.date_separator)
754 R.modifyState $ \ctx ->
755 let j = context_journal ctx in
759 (context_cons ctx $ Chart.Charted (journal_chart j) t)
760 (journal_content j)}}
765 , m ~ R.Error_State Error IO
767 => ParsecT s u m (ParsecT s u m ())
771 -- ** Read 'Journal' from a file
779 -> ExceptT [R.Error Error] IO (Journal j)
783 (liftM Right $ Text.IO.readFile path) $
784 \ko -> return $ Left $
785 [ R.Error_Custom (R.initialPos path) $ Error_reading_file path ko ]
786 >>= liftIO . R.runParserT_with_Error (journal path) ctx path
788 Left ko -> throwE $ ko
789 Right ok -> ExceptT $ return $ Right ok