1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE NamedFieldPuns #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE ScopedTypeVariables #-}
6 {-# LANGUAGE StandaloneDeriving #-}
7 {-# LANGUAGE TupleSections #-}
8 {-# LANGUAGE TypeFamilies #-}
9 module Hcompta.Format.JCC.Read where
11 import Control.Applicative ((<$>), (<*>), (<*))
12 import qualified Control.Exception as Exception
13 import Control.Monad (Monad(..), guard, liftM, join, void)
14 import Control.Monad.IO.Class (liftIO)
15 import Control.Monad.Trans.Except (ExceptT(..), throwE)
16 import Data.Time.LocalTime (TimeZone(..))
19 import Data.Char (Char)
20 import qualified Data.Char as Char
21 import Data.Either (Either(..))
22 import Data.Eq (Eq(..))
23 import Data.Ord (Ord(..))
24 import Data.Function (($), (.), id, const, flip)
25 import qualified Data.List as List
26 import Data.List.NonEmpty (NonEmpty(..))
27 import qualified Data.List.NonEmpty as NonEmpty
28 import Data.Map.Strict (Map)
29 import qualified Data.Map.Strict as Map
30 import Data.Maybe (Maybe(..), maybe)
31 import Data.Monoid (Monoid(..))
32 import Data.String (String, fromString)
33 import Data.Text (Text)
34 import qualified Data.Text.IO as Text.IO (readFile)
35 import qualified Data.Time.Calendar as Time
36 import qualified Data.Time.Clock as Time
37 import qualified Data.Time.LocalTime as Time
38 import Data.Typeable ()
39 import Prelude (Int, Integer, Num(..), fromIntegral)
40 import qualified System.FilePath.Posix as Path
41 import System.IO (IO, FilePath)
42 import qualified Text.Parsec as R hiding
55 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
56 import qualified Text.Parsec.Pos as R
57 import Text.Show (Show)
59 import Hcompta.Anchor ( Anchors(..) )
60 import qualified Hcompta.Account as Account
61 import Hcompta.Account ( Account_Tags(..)
65 import qualified Hcompta.Amount as Amount
66 import qualified Hcompta.Balance as Balance
67 import qualified Hcompta.Chart as Chart
68 import Hcompta.Date (Date)
69 import qualified Hcompta.Date as Date
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 qualified Hcompta.Lib.Regex as Regex
75 import qualified Hcompta.Lib.TreeMap as TreeMap
76 import qualified Hcompta.Polarize as Polarize
77 import qualified Hcompta.Posting as Posting
78 import Hcompta.Posting ( Posting_Tag(..)
83 import Hcompta.Tag (Tags(..))
84 import qualified Hcompta.Tag as Tag
85 import Hcompta.Transaction ( Transaction_Tags(..)
87 , Transaction_Anchor(..)
88 , Transaction_Anchors(..)
90 import qualified Hcompta.Transaction as Transaction
91 import qualified Hcompta.Unit as Unit
92 import qualified Hcompta.Filter.Date.Read as Filter.Date.Read
93 import Hcompta.Filter.Date.Read (Error(..))
95 import Hcompta.Format.JCC
97 -- * Type 'Read_Context'
101 { read_context_account_prefix :: !(Maybe Account)
102 , read_context_aliases_exact :: !(Map Account Account)
103 , read_context_aliases_joker :: ![(Account_Joker, Account)]
104 , read_context_aliases_regex :: ![(Regex, Account)]
105 , read_context_cons :: Charted Transaction -> c
106 , read_context_date :: !Date
107 , read_context_journal :: !(Journal j)
108 , read_context_unit :: !(Maybe Unit)
109 , read_context_year :: !Date.Year
114 => (Charted Transaction -> c)
117 read_context read_context_cons read_context_journal =
119 { read_context_account_prefix = Nothing
120 , read_context_aliases_exact = mempty
121 , read_context_aliases_joker = []
122 , read_context_aliases_regex = []
124 , read_context_date = Date.nil
125 , read_context_journal
126 , read_context_unit = Nothing
127 , read_context_year = Date.year Date.nil
130 -- * Type 'Read_Error'
133 = Read_Error_account_anchor_unknown R.SourcePos Account_Anchor
134 | Read_Error_account_anchor_not_unique R.SourcePos Account_Anchor
135 | Read_Error_date Date_Error
136 | Read_Error_transaction_not_equilibrated
140 , Balance.Unit_Sum Account
141 (Polarize.Polarized Quantity)
143 | Read_Error_virtual_transaction_not_equilibrated
147 , Balance.Unit_Sum Account
148 (Polarize.Polarized Quantity)
150 | Read_Error_reading_file FilePath Exception.IOException
151 | Read_Error_including_file FilePath [R.Error Read_Error]
154 -- * Read common patterns
156 is_space :: Char -> Bool
158 case Char.generalCategory c of
161 read_space :: Stream s m Char => ParsecT s u m Char
162 read_space = R.satisfy is_space
164 is_char :: Char -> Bool
166 case Char.generalCategory c of
167 Char.UppercaseLetter -> True
168 Char.LowercaseLetter -> True
169 Char.TitlecaseLetter -> True
170 Char.ModifierLetter -> True
171 Char.OtherLetter -> True
173 Char.NonSpacingMark -> True
174 Char.SpacingCombiningMark -> True
175 Char.EnclosingMark -> True
177 Char.DecimalNumber -> True
178 Char.LetterNumber -> True
179 Char.OtherNumber -> True
181 Char.ConnectorPunctuation -> True
182 Char.DashPunctuation -> True
183 Char.OpenPunctuation -> True
184 Char.ClosePunctuation -> True
185 Char.InitialQuote -> True
186 Char.FinalQuote -> True
187 Char.OtherPunctuation -> True
189 Char.MathSymbol -> True
190 Char.CurrencySymbol -> True
191 Char.ModifierSymbol -> True
192 Char.OtherSymbol -> True
195 Char.LineSeparator -> False
196 Char.ParagraphSeparator -> False
197 Char.Control -> False
199 Char.Surrogate -> False
200 Char.PrivateUse -> False
201 Char.NotAssigned -> False
202 read_char :: Stream s m Char => ParsecT s u m Char
203 read_char = R.satisfy is_char
205 is_char_active :: Char -> Bool
237 read_char_active :: Stream s m Char => ParsecT s u m Char
238 read_char_active = R.satisfy is_char_active
240 is_char_passive :: Char -> Bool
241 is_char_passive c = is_char c && not (is_char_active c)
242 read_char_passive :: Stream s m Char => ParsecT s u m Char
243 read_char_passive = R.satisfy is_char_passive
245 read_word :: Stream s m Char => ParsecT s u m Text
246 read_word = fromString <$> R.many read_char_passive
248 read_words :: Stream s m Char => ParsecT s u m [Text]
249 read_words = R.many_separated read_word read_space
251 read_name :: Stream s m Char => ParsecT s u m Text
252 read_name = fromString <$> R.many1 read_char_passive
254 read_tabulation :: Stream s m Char => ParsecT s u m Char
255 read_tabulation = R.char '\t'
257 read_hspace :: Stream s m Char => ParsecT s u m Char
258 read_hspace = R.char ' '
260 read_hspaces :: Stream s m Char => ParsecT s u m ()
261 read_hspaces = void $ R.many read_hspace
263 read_hspaces1 :: Stream s m Char => ParsecT s u m ()
264 read_hspaces1 = void $ R.many1 read_hspace
266 read_eol :: Stream s m Char => ParsecT s u m ()
267 read_eol = ((R.<|>) (void $ R.char '\n') (void $ R.try $ R.string "\r\n")) <?> "eol"
269 read_line :: Stream s m Char => ParsecT s u m Text
270 read_line = fromString <$>
271 R.manyTill read_char (R.lookAhead read_eol <|> R.eof)
272 -- R.many (R.notFollowedBy eol >> char)
276 read_account :: Stream s m Char => ParsecT s u m Account
278 Account.from_List <$> do
279 R.many1 (R.char read_account_section_sep >> read_account_section)
281 read_account_section :: Stream s m Char => ParsecT s u m Text
282 read_account_section = read_name
284 read_account_section_sep :: Char
285 read_account_section_sep = '/'
287 read_comment_prefix :: Char
288 read_comment_prefix = ';'
290 read_account_section_joker :: Stream s m Char => ParsecT s u m Account_Joker_Section
291 read_account_section_joker = do
292 n <- R.option Nothing $ (Just <$> read_account_section)
294 Nothing -> R.char read_account_section_sep >> return Account_Joker_Any
295 Just n' -> return $ Account_Joker_Section n'
297 read_account_joker :: Stream s m Char => ParsecT s u m Account_Joker
298 read_account_joker = do
299 R.notFollowedBy $ R.space_horizontal
300 R.many1_separated read_account_section_joker $ R.char read_account_section_sep
302 read_account_regex :: Stream s m Char => ParsecT s u m Regex
303 read_account_regex = do
304 re <- R.many1 $ R.satisfy (not . R.is_space_horizontal)
307 read_account_pattern :: Stream s m Char => ParsecT s u m Account_Pattern
308 read_account_pattern = do
310 [ Account_Pattern_Exact <$> (R.char '=' >> read_account)
311 , Account_Pattern_Joker <$> (R.char '*' >> read_account_joker)
312 , Account_Pattern_Regex <$> (R.option '~' (R.char '~') >> read_account_regex)
317 -- ** Read 'Account_Tag'
318 read_account_tag_prefix :: Char
319 read_account_tag_prefix = '.'
320 read_account_tag_sep :: Char
321 read_account_tag_sep = ':'
322 read_account_tag_value_prefix :: Char
323 read_account_tag_value_prefix = '='
325 read_account_tag :: Stream s m Char => ParsecT s u m Account_Tag
326 read_account_tag = (do
327 _ <- R.char read_account_tag_prefix
331 R.many (R.char read_account_tag_sep >> read_name)
334 (read_hspaces >> R.char read_transaction_tag_value_prefix >> read_hspaces >>
335 (List.concat <$> R.many (R.choice
336 [ R.string [read_account_tag_prefix , read_account_tag_prefix] >> return [read_account_tag_prefix]
337 , R.string [read_account_anchor_prefix, read_account_anchor_prefix] >> return [read_account_anchor_prefix]
338 , (\s c -> mappend s [c])
339 <$> R.many read_space
341 c /= read_account_tag_prefix
342 && c /= read_account_anchor_prefix
347 -- ** Read 'Account_Anchor'
348 read_account_anchor_prefix :: Char
349 read_account_anchor_prefix = '~'
350 read_account_anchor_sep :: Char
351 read_account_anchor_sep = ':'
353 read_account_anchor :: Stream s m Char => ParsecT s u m Account_Anchor
354 read_account_anchor = (do
355 _ <- R.char read_account_anchor_prefix
357 ps <- R.many (R.char read_account_anchor_sep >> read_name)
358 return $ Account.anchor (p:|ps)
359 ) <?> "account_anchor"
361 -- ** Read 'Account' 'Comment'
362 read_account_comment :: Stream s m Char => ParsecT s u m Comment
363 read_account_comment = read_comment
369 => Char -- ^ Integral grouping separator.
370 -> Char -- ^ Fractioning separator.
371 -> Char -- ^ Fractional grouping separator.
373 ( [String] -- integral
374 , [String] -- fractional
375 , Maybe Amount_Style_Fractioning -- fractioning
376 , Maybe Amount_Style_Grouping -- grouping_integral
377 , Maybe Amount_Style_Grouping -- grouping_fractional
379 read_quantity int_group_sep frac_sep frac_group_sep = do
380 (integral, grouping_integral) <- do
383 [] -> return ([], Nothing)
385 t <- R.many $ R.char int_group_sep >> R.many1 R.digit
387 return (digits, grouping_of_digits int_group_sep digits)
388 (fractional, fractioning, grouping_fractional) <-
391 _ -> R.option ([], Nothing, Nothing)) $ do
392 fractioning <- R.char frac_sep
394 t <- R.many $ R.char frac_group_sep >> R.many1 R.digit
396 return (digits, Just fractioning
397 , grouping_of_digits frac_group_sep $ List.reverse digits)
403 , grouping_fractional
406 grouping_of_digits :: Char -> [String] -> Maybe Amount_Style_Grouping
407 grouping_of_digits group_sep digits =
412 Amount_Style_Grouping group_sep $
413 canonicalize_grouping $
414 List.map List.length $ digits
415 canonicalize_grouping :: [Int] -> [Int]
416 canonicalize_grouping groups =
417 List.foldl' -- NOTE: remove duplicates at beginning and reverse.
418 (\acc l0 -> case acc of
419 l1:_ -> if l0 == l1 then acc else l0:acc
421 case groups of -- NOTE: keep only longer at beginning.
422 l0:l1:t -> if l0 > l1 then groups else l1:t
427 read_unit :: Stream s m Char => ParsecT s u m Unit
429 (quoted <|> unquoted) <?> "unit"
431 unquoted :: Stream s m Char => ParsecT s u m Unit
436 case Char.generalCategory c of
437 Char.CurrencySymbol -> True
438 Char.LowercaseLetter -> True
439 Char.ModifierLetter -> True
440 Char.OtherLetter -> True
441 Char.TitlecaseLetter -> True
442 Char.UppercaseLetter -> True
444 quoted :: Stream s m Char => ParsecT s u m Unit
447 R.between (R.char '"') (R.char '"') $
455 => ParsecT s u m (Amount_Styled Amount)
457 left_signing <- read_sign
459 R.option Nothing $ do
461 s <- R.many $ R.space_horizontal
462 return $ Just $ (u, not $ List.null s)
465 ( amount_style_integral
466 , amount_style_fractional
467 , amount_style_fractioning
468 , amount_style_grouping_integral
469 , amount_style_grouping_fractional
472 [ read_quantity '_' ',' '_' <* (R.notFollowedBy $ R.oneOf ",._")
473 , read_quantity '_' '.' '_' <* (R.notFollowedBy $ R.oneOf ",._")
474 , read_quantity ',' '.' '_' <* (R.notFollowedBy $ R.oneOf ",._")
475 , read_quantity '.' ',' '_' <* (R.notFollowedBy $ R.oneOf ",._")
477 let int = List.concat amount_style_integral
478 let frac = List.concat amount_style_fractional
479 let precision = List.length frac
480 guard (precision <= 255)
481 let mantissa = R.integer_of_digits 10 $ int `mappend` frac
483 ( Data.Decimal.Decimal
484 (fromIntegral precision)
487 { amount_style_fractioning
488 , amount_style_grouping_integral
489 , amount_style_grouping_fractional
493 , amount_style_unit_side
494 , amount_style_unit_spaced ) <-
497 return (u, Just Amount_Style_Side_Left, Just s)
499 R.option (Unit.unit_empty, Nothing, Nothing) $ R.try $ do
500 s <- R.many R.space_horizontal
504 , Just Amount_Style_Side_Right
505 , Just $ not $ List.null s )
508 { amount_style_unit_side
509 , amount_style_unit_spaced
512 { amount_quantity = left_signing qty
517 -- | Parse either '-' into 'negate', or '+' or '' into 'id'.
518 read_sign :: (Stream s m Char, Num i) => ParsecT s u m (i -> i)
520 (R.char '-' >> return negate)
521 <|> (R.char '+' >> return id)
526 type Date_Error = Filter.Date.Read.Error
528 -- | Read a 'Date' in @[YYYY[/-]]MM[/-]DD[_HH:MM[:SS][TZ]]@ format.
530 :: (Stream s (R.Error_State e m) Char, Monad m)
531 => (Date_Error -> e) -> Maybe Integer
532 -> ParsecT s u (R.Error_State e m) Date
533 read_date err def_year = (do
534 let read_2_or_1_digits = R.try (R.count 2 R.digit) <|> R.count 1 R.digit
535 n0 <- R.many1 R.digit
536 day_sep <- R.char read_date_ymd_sep
537 n1 <- read_2_or_1_digits
538 n2 <- R.option Nothing $ R.try $ do
540 Just <$> read_2_or_1_digits
542 case (n2, def_year) of
543 (Nothing, Nothing) -> R.fail_with "date" (err $ Error_year_or_day_is_missing)
544 (Nothing, Just year) -> return (year, n0, n1)
545 (Just d, _) -> return (R.integer_of_digits 10 n0, n1, d)
546 let month = fromInteger $ R.integer_of_digits 10 m
547 let dom = fromInteger $ R.integer_of_digits 10 d
548 day <- case Time.fromGregorianValid year month dom of
549 Nothing -> R.fail_with "date" (err $ Error_invalid_date (year, month, dom))
550 Just day -> return day
551 (hour, minu, sec, tz) <-
552 R.option (0, 0, 0, Time.utc) $ R.try $ do
554 hour <- read_2_or_1_digits
555 sep <- R.char read_hour_separator
556 minu <- read_2_or_1_digits
557 sec <- R.option Nothing $ R.try $ do
559 Just <$> read_2_or_1_digits
560 tz <- R.option Time.utc $ R.try $
563 ( fromInteger $ R.integer_of_digits 10 hour
564 , fromInteger $ R.integer_of_digits 10 minu
565 , maybe 0 (R.integer_of_digits 10) sec
567 tod <- case Time.makeTimeOfDayValid hour minu (fromInteger sec) of
568 Nothing -> R.fail_with "date" (err $ Error_invalid_time_of_day (hour, minu, sec))
569 Just tod -> return tod
570 return $ Time.localTimeToUTC tz (Time.LocalTime day tod)
573 -- | Separator for year, month and day: "-".
574 read_date_ymd_sep :: Char
575 read_date_ymd_sep = '-'
577 -- | Separator for hour, minute and second: ":".
578 read_hour_separator :: Char
579 read_hour_separator = ':'
581 read_time_zone :: Stream s m Char => ParsecT s u m TimeZone
582 read_time_zone = Filter.Date.Read.time_zone
584 read_time_zone_digits :: Stream s m Char => ParsecT s u m TimeZone
585 read_time_zone_digits = Filter.Date.Read.time_zone_digits
591 => ParsecT s u m Comment
593 _ <- R.char read_comment_prefix
595 R.manyTill R.anyChar (R.lookAhead (R.try read_eol <|> R.eof))
598 -- ** Read 'Comment's
602 => ParsecT s u m [Comment]
606 R.many1_separated read_comment
607 (read_eol >> read_hspaces)
616 , Stream s (R.Error_State Read_Error m) Char
617 ) => ParsecT s (Read_Context c j)
618 (R.Error_State Read_Error m)
621 posting_sourcepos <- R.getPosition
623 , posting_account_anchor ) <-
625 [ (,Nothing) <$> read_account
627 anchor <- read_account_anchor
629 let anchors = Chart.chart_anchors $
630 journal_chart $ read_context_journal ctx
631 case Map.lookup anchor anchors of
633 sa <- R.option Nothing $ Just <$> read_account
634 return $ ( a:|mappend as (maybe [] NonEmpty.toList sa)
635 , Just (anchor, sa) )
636 Nothing -> R.fail_with "account anchor"
637 (Read_Error_account_anchor_unknown posting_sourcepos anchor)
638 ] <?> "posting_account"
640 R.option mempty $ R.try $ do
641 (style, amt) <- read_hspaces1 >> read_amount
642 ctx <- flip liftM R.getState $ \ctx ->
644 { read_context_journal=
645 let jnl = read_context_journal ctx in
647 { journal_amount_styles =
648 let Amount_Styles styles = journal_amount_styles jnl in
650 Map.insertWith mappend
658 case amount_unit amt of
659 u | u == Unit.unit_empty ->
660 maybe u id $ read_context_unit ctx
666 , posting_comments ) <- read_posting_attributes
669 , posting_account_anchor
671 , posting_anchors = Posting_Anchors posting_anchors
672 , posting_tags = Posting_Tags posting_tags
679 read_posting_attributes
680 :: Stream s (R.Error_State Read_Error m) Char
681 => ParsecT s (Read_Context c j)
682 (R.Error_State Read_Error m)
683 (Tags, Anchors, [Comment])
684 read_posting_attributes =
685 R.option mempty $ R.try $ do
686 _ <- R.many (R.try (read_hspaces >> read_eol))
688 [ read_hspaces1 >> read_posting_anchor >>= \(Posting_Anchor p) -> do
689 (tags, Anchors anchors, cmts) <- read_posting_attributes
690 return (tags, Anchors (Map.insert p () anchors), cmts)
691 , read_hspaces1 >> read_posting_tag >>= \(Posting_Tag (p, v)) -> do
692 (Tags tags, anchors, cmts) <- read_posting_attributes
693 return (Tags (Map.insertWith mappend p [v] tags), anchors, cmts)
694 , read_hspaces >> read_comment >>= \c -> do
695 (tags, anchors, cmts) <- read_posting_attributes
696 return (tags, anchors, c:cmts)
699 read_amount_sep :: Char
700 read_amount_sep = '+'
702 read_posting_comment :: Stream s m Char => ParsecT s u m Comment
703 read_posting_comment = read_comment
705 -- ** Read 'Posting_Tag'
706 read_posting_tag :: Stream s m Char => ParsecT s u m Posting_Tag
708 (liftM (\(Transaction_Tag t) -> Posting_Tag t)
709 read_transaction_tag) <?> "posting_tag"
711 -- ** Read 'Posting_Anchor'
712 read_posting_anchor :: Stream s m Char => ParsecT s u m Posting_Anchor
713 read_posting_anchor = (do
714 _ <- R.char read_transaction_anchor_prefix
716 NonEmpty.fromList <$>
717 R.many1 (R.char read_transaction_anchor_sep >> read_name)
718 ) <?> "posting_anchor"
720 -- * Read 'Transaction'
725 , Stream s (R.Error_State Read_Error m) Char
726 ) => ParsecT s (Read_Context c j)
727 (R.Error_State Read_Error m)
729 read_transaction = (do
730 transaction_sourcepos <- R.getPosition
732 date_ <- read_date Read_Error_date (Just $ read_context_year ctx)
734 R.option [] $ R.try $ do
736 _ <- R.char read_transaction_date_sep
739 (read_date Read_Error_date (Just $ read_context_year ctx)) $
742 >> R.char read_transaction_date_sep
744 let transaction_dates = (date_, dates_)
746 transaction_wording <- read_transaction_wording
748 , transaction_anchors
749 , transaction_comments
750 ) <- read_transaction_attributes
751 transaction_postings_unchecked <-
752 postings_by_account <$> read_postings
753 let transaction_unchecked =
755 { transaction_anchors = Transaction_Anchors transaction_anchors
756 , transaction_tags = Transaction_Tags transaction_tags
757 , transaction_comments
759 , transaction_wording
760 , transaction_postings = transaction_postings_unchecked
761 , transaction_sourcepos
763 let styles = journal_amount_styles $ read_context_journal ctx
764 transaction_postings <-
765 case Balance.infer_equilibrium transaction_postings_unchecked of
766 (_, Left ko) -> R.fail_with "transaction infer_equilibrium" $
767 Read_Error_transaction_not_equilibrated styles transaction_unchecked ko
768 (_bal, Right ok) -> return ok
770 transaction_unchecked
771 { transaction_postings
775 read_transaction_attributes
776 :: Stream s (R.Error_State Read_Error m) Char
777 => ParsecT s (Read_Context c j)
778 (R.Error_State Read_Error m)
779 (Tags, Anchors, [Comment])
780 read_transaction_attributes =
781 R.option mempty $ R.try $ do
782 _ <- R.many (R.try (read_hspaces >> read_eol))
784 [ read_hspaces1 >> read_transaction_anchor >>= \(Transaction_Anchor p) -> do
785 (tags, Anchors anchors, cmts) <- read_transaction_attributes
786 return (tags, Anchors (Map.insert p () anchors), cmts)
787 , read_hspaces1 >> read_transaction_tag >>= \(Transaction_Tag (p, v)) -> do
788 (Tags tags, anchors, cmts) <- read_transaction_attributes
789 return (Tags (Map.insertWith mappend p [v] tags), anchors, cmts)
790 , read_hspaces >> read_comment >>= \c -> do
791 (tags, anchors, cmts) <- read_transaction_attributes
792 return (tags, anchors, c:cmts)
796 (Consable c j, Monad m, Stream s (R.Error_State Read_Error m) Char)
797 => ParsecT s (Read_Context c j) (R.Error_State Read_Error m) [Posting]
798 read_postings = R.many $ R.try (read_hspaces >> read_eol >> read_hspaces1 >> read_posting)
800 read_transaction_date_sep :: Char
801 read_transaction_date_sep = '='
803 read_transaction_wording
805 => ParsecT s u m Wording
806 read_transaction_wording =
807 fromString . List.concat <$> (do
809 s <- R.many read_hspace
810 c <- R.satisfy $ \c ->
811 c /= read_transaction_tag_prefix &&
812 c /= read_transaction_anchor_prefix &&
813 c /= read_comment_prefix &&
815 cs <- R.many (R.satisfy is_char)
816 return $ mappend s (c:cs)
819 -- ** Read 'Transaction_Anchor'
821 read_transaction_anchor_prefix :: Char
822 read_transaction_anchor_prefix = '@'
823 read_transaction_anchor_sep :: Char
824 read_transaction_anchor_sep = ':'
826 read_transaction_anchor :: Stream s m Char => ParsecT s u m Transaction_Anchor
827 read_transaction_anchor = (do
828 _ <- R.char read_transaction_anchor_prefix
830 Transaction.anchor <$>
832 R.many (R.char read_transaction_anchor_sep >> read_name)
833 ) <?> "transaction_anchor"
835 -- ** Read 'Transaction_Tag'
837 read_transaction_tag_prefix :: Char
838 read_transaction_tag_prefix = '#'
839 read_transaction_tag_sep :: Char
840 read_transaction_tag_sep = ':'
841 read_transaction_tag_value_prefix :: Char
842 read_transaction_tag_value_prefix = '='
844 read_transaction_tag :: Stream s m Char => ParsecT s u m Transaction_Tag
845 read_transaction_tag = (do
846 _ <- R.char read_transaction_tag_prefix
851 R.many (R.char read_transaction_tag_sep >> read_name)
852 <*> (R.option "" $ R.try $ do
854 _ <- R.char read_transaction_tag_value_prefix
856 read_transaction_tag_value)
857 ) <?> "transaction_tag"
860 read_transaction_tag_value
862 => ParsecT s u m Tag.Value
863 read_transaction_tag_value =
864 fromString . List.concat <$> do
866 s <- R.many read_hspace
867 c <- R.satisfy $ \c ->
868 c /= read_transaction_tag_prefix &&
869 c /= read_transaction_anchor_prefix &&
870 c /= read_comment_prefix &&
872 cs <- R.many (R.satisfy is_char)
873 return $ mappend s (c:cs)
875 -- ** Read 'Transaction' 'Comment'
876 read_transaction_comment :: Stream s m Char => ParsecT s u m Comment
877 read_transaction_comment = read_comment
882 :: (Consable c j, Stream s m Char)
883 => ParsecT s (Read_Context c j) m ()
884 read_directive_alias = do
885 _ <- R.string "alias"
886 R.skipMany1 $ R.space_horizontal
887 pattern <- read_account_pattern
894 Account_Pattern_Exact acct ->
895 R.modifyState $ \ctx -> ctx{read_context_aliases_exact=
896 Map.insert acct repl $ read_context_aliases_exact ctx}
897 Account_Pattern_Joker jokr ->
898 R.modifyState $ \ctx -> ctx{read_context_aliases_joker=
899 (jokr, repl):read_context_aliases_joker ctx}
900 Account_Pattern_Regex regx ->
901 R.modifyState $ \ctx -> ctx{read_context_aliases_regex=
902 (regx, repl):read_context_aliases_regex ctx}
906 :: (Consable c j, Stream s m Char)
907 => ParsecT s (Read_Context c j) m ()
908 read_default_year = (do
909 year <- R.integer_of_digits 10 <$> R.many1 R.digit
911 read_context_ <- R.getState
912 R.setState read_context_{read_context_year=year}
915 read_default_unit_and_style
918 => ParsecT s (Read_Context c j) m ()
919 read_default_unit_and_style = (do
920 (sty, amt) <- read_amount
923 let unit = Amount.amount_unit amt
925 { read_context_journal =
926 let jnl = read_context_journal ctx in
928 { journal_amount_styles =
929 let Amount_Styles styles =
930 journal_amount_styles jnl in
932 Map.insertWith const unit sty styles
934 , read_context_unit = Just unit
936 ) <?> "default unit and style"
941 , Stream s (R.Error_State Read_Error IO) Char
942 ) => ParsecT s (Read_Context c j) (R.Error_State Read_Error IO) ()
944 sourcepos <- R.getPosition
945 filename <- R.manyTill R.anyChar (R.lookAhead (R.try read_eol <|> R.eof))
946 read_context_including <- R.getState
947 let journal_including = read_context_journal read_context_including
948 let cwd = Path.takeDirectory (R.sourceName sourcepos)
949 journal_file <- liftIO $ Path.abs cwd filename
951 join $ liftIO $ Exception.catch
952 (liftM return $ Text.IO.readFile journal_file)
953 (return . R.fail_with "include reading" . Read_Error_reading_file journal_file)
954 (journal_included, read_context_included) <- do
956 R.runParserT_with_Error
957 (R.and_state $ read_journal_rec journal_file)
958 read_context_including
959 { read_context_journal=
961 { journal_chart = journal_chart journal_including
962 , journal_amount_styles = journal_amount_styles journal_including
967 Right ok -> return ok
968 Left ko -> R.fail_with "include parsing" (Read_Error_including_file journal_file ko)
970 read_context_included
971 { read_context_journal=
974 journal_included{journal_files=[journal_file]} :
975 journal_includes journal_including
977 journal_chart journal_included
978 , journal_amount_styles=
979 journal_amount_styles journal_included
988 , Stream s (R.Error_State Read_Error IO) Char
989 ) => ParsecT s (Read_Context c j)
990 (R.Error_State Read_Error IO)
993 -- sourcepos <- R.getPosition
998 , _chart_comments ) <-
999 fields acct mempty mempty mempty
1000 let chart_accounts =
1001 TreeMap.singleton acct $
1002 Account_Tags chart_tags
1004 let j = read_context_journal ctx
1006 ctx{read_context_journal=
1011 { Chart.chart_accounts
1012 -- , Chart.chart_tags
1013 , Chart.chart_anchors
1025 [ read_hspaces1 >> read_account_comment >>= \c ->
1026 fields acct tags anchors (c:cmts)
1027 , read_hspaces1 >> read_account_tag >>= \(Account_Tag (p, v)) ->
1028 fields acct (Tags $ Map.insertWith mappend p [v] tagm) anchors cmts
1029 , read_hspaces1 >> read_account_anchor >>= \anchor ->
1030 case Map.insertLookupWithKey (\_k n _o -> n) anchor acct anchors of
1031 (Nothing, m) -> fields acct tags m cmts
1033 sourcepos <- R.getPosition
1034 R.fail_with "account anchor not unique"
1035 (Read_Error_account_anchor_not_unique sourcepos anchor)
1036 , read_hspaces >> read_eol >>
1037 fields acct tags anchors cmts
1038 , return (tags, anchors, cmts)
1046 , Stream s (R.Error_State Read_Error IO) Char
1048 -> ParsecT s (Read_Context c j)
1049 (R.Error_State Read_Error IO)
1051 read_journal filepath = (do
1052 currentLocalTime <- liftIO $
1054 <$> Time.getCurrentTimeZone
1055 <*> Time.getCurrentTime
1056 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
1058 R.setState $ ctx{read_context_year=currentLocalYear}
1059 read_journal_rec filepath
1065 , Stream s (R.Error_State Read_Error IO) Char
1068 -> ParsecT s (Read_Context c j)
1069 (R.Error_State Read_Error IO)
1071 read_journal_rec journal_file = do
1072 last_read_time <- liftIO Date.now
1080 journal_ <- read_context_journal <$> R.getState
1083 { journal_files = [journal_file]
1084 , journal_includes = List.reverse $ journal_includes journal_
1085 , journal_last_read_time = last_read_time
1090 => ParsecT s u m (ParsecT s u m ())
1093 R.skipMany (read_hspaces >> read_eol)
1095 R.skipMany (read_hspaces >> read_eol)
1096 R.try (read_hspaces >> R.eof) <|> loop r
1100 , u ~ Read_Context c j
1101 , m ~ R.Error_State Read_Error IO
1103 => ParsecT s u m (ParsecT s u m ())
1106 _ <- R.lookAhead (R.try $ R.char read_comment_prefix)
1108 _cmts <- read_comments
1110 R.modifyState $ \ctx ->
1111 let j = read_context_journal ctx in
1112 ctx{read_context_journal=
1114 mcons (read_context_filter ctx) cmts $
1122 , u ~ Read_Context c j
1123 , m ~ R.Error_State Read_Error IO
1125 => ParsecT s u m (ParsecT s u m ())
1127 let choice s = R.string s >> R.skipMany1 R.space_horizontal
1129 [ choice "Y" >> return read_default_year
1130 , choice "D" >> return read_default_unit_and_style
1131 , choice "!include" >> return read_include
1136 , u ~ Read_Context c j
1137 , m ~ R.Error_State Read_Error IO
1139 => ParsecT s u m (ParsecT s u m ())
1140 jump_transaction = do
1141 _ <- R.lookAhead $ R.try (R.many1 R.digit >> R.char read_date_ymd_sep)
1143 t <- read_transaction
1144 R.modifyState $ \ctx ->
1145 let j = read_context_journal ctx in
1146 ctx{read_context_journal=
1149 (read_context_cons ctx $
1150 Chart.Charted (journal_chart j) t)
1151 (journal_content j)}}
1155 , u ~ Read_Context c j
1156 , m ~ R.Error_State Read_Error IO
1158 => ParsecT s u m (ParsecT s u m ())
1165 :: (Consable c j, Monoid j)
1168 -> ExceptT [R.Error Read_Error] IO (Journal j)
1172 (liftM Right $ Text.IO.readFile path) $
1173 \ko -> return $ Left $
1174 [R.Error_Custom (R.initialPos path) $
1175 Read_Error_reading_file path ko]
1176 >>= liftIO . R.runParserT_with_Error
1177 (read_journal path) ctx path
1179 Left ko -> throwE $ ko
1180 Right ok -> ExceptT $ return $ Right ok