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.Ledger.Read where
11 import Control.Applicative ((<$>), (<*>), (<*))
12 import Control.Arrow ((***), first)
13 import qualified Control.Exception as Exception
14 import Control.Monad (Monad(..), guard, liftM, join, forM, void)
15 import Control.Monad.IO.Class (liftIO)
16 import Control.Monad.Trans.Except (ExceptT(..), throwE)
17 import Data.Time.LocalTime (TimeZone(..))
20 import Data.Char (Char)
21 import qualified Data.Char as Char
22 import Data.Either (Either(..), either)
23 import Data.Eq (Eq(..))
24 import Data.Ord (Ord(..))
25 import Data.Function (($), (.), id, const, flip)
26 import Data.Functor (Functor(..))
27 import qualified Data.List as List
28 import Data.List.NonEmpty (NonEmpty(..))
29 import qualified Data.List.NonEmpty as NonEmpty
30 import Data.Map.Strict (Map)
31 import qualified Data.Map.Strict as Map
32 import Data.Maybe (Maybe(..), fromMaybe, maybe)
33 import Data.Monoid (Monoid(..))
34 import Data.String (String, fromString)
35 import qualified Data.Text as Text
36 import Data.Text (Text)
37 import qualified Data.Text.IO as Text.IO (readFile)
38 import qualified Data.Time.Calendar as Time
39 import qualified Data.Time.Clock as Time
40 import qualified Data.Time.LocalTime as Time
41 import Data.Typeable ()
42 import Prelude (Int, Integer, Num(..), fromIntegral)
43 import qualified System.FilePath.Posix as Path
44 import System.IO (IO, FilePath)
45 import qualified Text.Parsec as R hiding
58 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
59 import qualified Text.Parsec.Pos as R
60 import Text.Show (Show)
62 import qualified Hcompta.Account as Account
63 import Hcompta.Account (Account_Tags(..))
64 import qualified Hcompta.Amount as Amount
65 import qualified Hcompta.Balance as Balance
66 import qualified Hcompta.Chart as Chart
67 import Hcompta.Date (Date)
68 import qualified Hcompta.Date as Date
69 import Hcompta.Lib.Consable (Consable(..))
70 import qualified Hcompta.Lib.Parsec as R
71 import qualified Hcompta.Lib.Path as Path
72 import Hcompta.Lib.Regex (Regex)
73 import qualified Hcompta.Lib.Regex as Regex
74 import qualified Hcompta.Lib.TreeMap as TreeMap
75 import qualified Hcompta.Polarize as Polarize
76 import Hcompta.Posting (Posting_Tags(..))
77 import qualified Hcompta.Quantity as Quantity
78 import Hcompta.Tag (Tag, Tags(..))
79 import qualified Hcompta.Tag as Tag
80 import Hcompta.Transaction (Transaction_Tags(..))
81 import qualified Hcompta.Unit as Unit
82 import qualified Hcompta.Filter.Date.Read as Filter.Date.Read
83 import Hcompta.Filter.Date.Read (Error(..))
85 import Hcompta.Format.Ledger
87 -- * Type 'Read_Context'
91 { read_context_account_prefix :: !(Maybe Account)
92 , read_context_aliases_exact :: !(Map Account Account)
93 , read_context_aliases_joker :: ![(Account_Joker, Account)]
94 , read_context_aliases_regex :: ![(Regex, Account)]
95 , read_context_cons :: Charted Transaction -> c
96 , read_context_date :: !Date
97 , read_context_journal :: !(Journal j)
98 , read_context_unit :: !(Maybe Unit)
99 , read_context_year :: !Date.Year
104 => (Charted Transaction -> c)
107 read_context read_context_cons read_context_journal =
109 { read_context_account_prefix = Nothing
110 , read_context_aliases_exact = mempty
111 , read_context_aliases_joker = []
112 , read_context_aliases_regex = []
114 , read_context_date = Date.nil
115 , read_context_journal
116 , read_context_unit = Nothing
117 , read_context_year = Date.year Date.nil
120 -- * Type 'Read_Error'
123 = Read_Error_date Date_Error
124 | Read_Error_transaction_not_equilibrated
128 , Balance.Unit_Sum Account
129 (Polarize.Polarized Quantity)
131 | Read_Error_virtual_transaction_not_equilibrated
135 , Balance.Unit_Sum Account
136 (Polarize.Polarized Quantity)
138 | Read_Error_reading_file FilePath Exception.IOException
139 | Read_Error_including_file FilePath [R.Error Read_Error]
142 -- * Read common patterns
144 read_hspaces :: Stream s m Char => ParsecT s u m ()
145 read_hspaces = R.skipMany R.space_horizontal
149 read_account :: Stream s m Char => ParsecT s u m Account
151 R.notFollowedBy $ R.space_horizontal
152 Account.from_List <$> do
153 R.many1_separated read_account_section $ R.char read_account_section_sep
155 read_account_section :: Stream s m Char => ParsecT s u m Text
156 read_account_section = do
157 fromString <$> (R.many1 $ R.try account_name_char)
159 account_name_char :: Stream s m Char => ParsecT s u m Char
160 account_name_char = do
163 _ | c == read_comment_begin -> R.parserZero
164 _ | c == read_account_section_sep -> R.parserZero
165 _ | c /= '\t' && R.is_space_horizontal c -> do
166 _ <- R.notFollowedBy $ R.space_horizontal
167 return c <* (R.lookAhead $ R.try $
168 ( R.try (R.char read_account_section_sep)
169 <|> account_name_char
171 _ | not (Char.isSpace c) -> return c
174 read_account_section_sep :: Char
175 read_account_section_sep = ':'
177 read_comment_begin :: Char
178 read_comment_begin = ';'
180 read_account_section_joker :: Stream s m Char => ParsecT s u m Account_Joker_Section
181 read_account_section_joker = do
182 n <- R.option Nothing $ (Just <$> read_account_section)
184 Nothing -> R.char read_account_section_sep >> return Account_Joker_Any
185 Just n' -> return $ Account_Joker_Section n'
187 read_account_joker :: Stream s m Char => ParsecT s u m Account_Joker
188 read_account_joker = do
189 R.notFollowedBy $ R.space_horizontal
190 R.many1_separated read_account_section_joker $ R.char read_account_section_sep
192 read_account_regex :: Stream s m Char => ParsecT s u m Regex
193 read_account_regex = do
194 re <- R.many1 $ R.satisfy (not . R.is_space_horizontal)
197 read_account_pattern :: Stream s m Char => ParsecT s u m Account_Pattern
198 read_account_pattern = do
200 [ Account_Pattern_Exact <$> (R.char '=' >> read_account)
201 , Account_Pattern_Joker <$> (R.char '*' >> read_account_joker)
202 , Account_Pattern_Regex <$> (R.option '~' (R.char '~') >> read_account_regex)
209 => Char -- ^ Integral grouping separator.
210 -> Char -- ^ Fractioning separator.
211 -> Char -- ^ Fractional grouping separator.
213 ( [String] -- integral
214 , [String] -- fractional
215 , Maybe Amount_Style_Fractioning -- fractioning
216 , Maybe Amount_Style_Grouping -- grouping_integral
217 , Maybe Amount_Style_Grouping -- grouping_fractional
219 read_quantity int_group_sep frac_sep frac_group_sep = do
220 (integral, grouping_integral) <- do
223 [] -> return ([], Nothing)
225 t <- R.many $ R.char int_group_sep >> R.many1 R.digit
227 return (digits, grouping_of_digits int_group_sep digits)
228 (fractional, fractioning, grouping_fractional) <-
231 _ -> R.option ([], Nothing, Nothing)) $ do
232 fractioning <- R.char frac_sep
234 t <- R.many $ R.char frac_group_sep >> R.many1 R.digit
236 return (digits, Just fractioning
237 , grouping_of_digits frac_group_sep $ List.reverse digits)
243 , grouping_fractional
246 grouping_of_digits :: Char -> [String] -> Maybe Amount_Style_Grouping
247 grouping_of_digits group_sep digits =
252 Amount_Style_Grouping group_sep $
253 canonicalize_grouping $
254 List.map List.length $ digits
255 canonicalize_grouping :: [Int] -> [Int]
256 canonicalize_grouping groups =
257 List.foldl' -- NOTE: remove duplicates at beginning and reverse.
258 (\acc l0 -> case acc of
259 l1:_ -> if l0 == l1 then acc else l0:acc
261 case groups of -- NOTE: keep only longer at beginning.
262 l0:l1:t -> if l0 > l1 then groups else l1:t
267 read_unit :: Stream s m Char => ParsecT s u m Unit
269 (quoted <|> unquoted) <?> "unit"
271 unquoted :: Stream s m Char => ParsecT s u m Unit
276 case Char.generalCategory c of
277 Char.CurrencySymbol -> True
278 Char.LowercaseLetter -> True
279 Char.ModifierLetter -> True
280 Char.OtherLetter -> True
281 Char.TitlecaseLetter -> True
282 Char.UppercaseLetter -> True
284 quoted :: Stream s m Char => ParsecT s u m Unit
287 R.between (R.char '"') (R.char '"') $
295 => ParsecT s u m (Amount_Styled Amount)
297 left_signing <- read_sign
299 R.option Nothing $ do
301 s <- R.many $ R.space_horizontal
302 return $ Just $ (u, not $ List.null s)
305 ( amount_style_integral
306 , amount_style_fractional
307 , amount_style_fractioning
308 , amount_style_grouping_integral
309 , amount_style_grouping_fractional
312 [ read_quantity '_' ',' '_' <* (R.notFollowedBy $ R.oneOf ",._")
313 , read_quantity '_' '.' '_' <* (R.notFollowedBy $ R.oneOf ",._")
314 , read_quantity ',' '.' '_' <* (R.notFollowedBy $ R.oneOf ",._")
315 , read_quantity '.' ',' '_' <* (R.notFollowedBy $ R.oneOf ",._")
317 let int = List.concat amount_style_integral
318 let frac = List.concat amount_style_fractional
319 let precision = List.length frac
320 guard (precision <= 255)
321 let mantissa = R.integer_of_digits 10 $ int `mappend` frac
323 ( Data.Decimal.Decimal
324 (fromIntegral precision)
327 { amount_style_fractioning
328 , amount_style_grouping_integral
329 , amount_style_grouping_fractional
333 , amount_style_unit_side
334 , amount_style_unit_spaced ) <-
337 return (u, Just Amount_Style_Side_Left, Just s)
339 R.option (Unit.unit_empty, Nothing, Nothing) $ R.try $ do
340 s <- R.many R.space_horizontal
344 , Just Amount_Style_Side_Right
345 , Just $ not $ List.null s )
348 { amount_style_unit_side
349 , amount_style_unit_spaced
352 { amount_quantity = left_signing qty
357 -- | Parse either '-' into 'negate', or '+' or '' into 'id'.
358 read_sign :: (Stream s m Char, Num i) => ParsecT s u m (i -> i)
360 (R.char '-' >> return negate)
361 <|> (R.char '+' >> return id)
366 type Date_Error = Filter.Date.Read.Error
368 -- | Read a 'Date' in @[YYYY[/-]]MM[/-]DD[_HH:MM[:SS][TZ]]@ format.
370 :: (Stream s (R.Error_State e m) Char, Monad m)
371 => (Date_Error -> e) -> Maybe Integer
372 -> ParsecT s u (R.Error_State e m) Date
373 read_date err def_year = (do
374 let read_2_or_1_digits = R.try (R.count 2 R.digit) <|> R.count 1 R.digit
375 n0 <- R.many1 R.digit
376 day_sep <- read_date_separator
377 n1 <- read_2_or_1_digits
378 n2 <- R.option Nothing $ R.try $ do
380 Just <$> read_2_or_1_digits
382 case (n2, def_year) of
383 (Nothing, Nothing) -> R.fail_with "date" (err $ Error_year_or_day_is_missing)
384 (Nothing, Just year) -> return (year, n0, n1)
385 (Just d, _) -> return (R.integer_of_digits 10 n0, n1, d)
386 let month = fromInteger $ R.integer_of_digits 10 m
387 let dom = fromInteger $ R.integer_of_digits 10 d
388 day <- case Time.fromGregorianValid year month dom of
389 Nothing -> R.fail_with "date" (err $ Error_invalid_date (year, month, dom))
390 Just day -> return day
391 (hour, minu, sec, tz) <-
392 R.option (0, 0, 0, Time.utc) $ R.try $ do
394 hour <- read_2_or_1_digits
395 sep <- R.char read_hour_separator
396 minu <- read_2_or_1_digits
397 sec <- R.option Nothing $ R.try $ do
399 Just <$> read_2_or_1_digits
400 tz <- R.option Time.utc $ R.try $
403 ( fromInteger $ R.integer_of_digits 10 hour
404 , fromInteger $ R.integer_of_digits 10 minu
405 , maybe 0 (R.integer_of_digits 10) sec
407 tod <- case Time.makeTimeOfDayValid hour minu (fromInteger sec) of
408 Nothing -> R.fail_with "date" (err $ Error_invalid_time_of_day (hour, minu, sec))
409 Just tod -> return tod
410 return $ Time.localTimeToUTC tz (Time.LocalTime day tod)
413 -- | Separator for year, month and day: "/" or "-".
414 read_date_separator :: Stream s m Char => ParsecT s u m Char
415 read_date_separator = R.char '/' <|> R.char '-'
417 -- | Separator for hour, minute and second: ":".
418 read_hour_separator :: Char
419 read_hour_separator = ':'
421 read_time_zone :: Stream s m Char => ParsecT s u m TimeZone
422 read_time_zone = Filter.Date.Read.time_zone
424 read_time_zone_digits :: Stream s m Char => ParsecT s u m TimeZone
425 read_time_zone_digits = Filter.Date.Read.time_zone_digits
431 => ParsecT s u m Comment
433 _ <- R.char read_comment_begin
435 R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
438 -- ** Read 'Comment's
442 => ParsecT s u m [Comment]
446 R.many1_separated read_comment
447 (R.new_line >> read_hspaces)
453 read_tag_value_sep :: Char
454 read_tag_value_sep = ':'
459 read_tag_path_section_char
461 => ParsecT s u m Char
462 read_tag_path_section_char =
463 R.satisfy (\c -> c /= read_tag_value_sep
465 && not (Char.isSpace c))
467 read_tag :: Stream s m Char => ParsecT s u m Tag
468 read_tag = ((,) <$> read_tag_path <*> read_tag_value) <?> "tag"
470 read_tag_path :: Stream s m Char => ParsecT s u m Tag.Path
472 NonEmpty.fromList <$> do
473 R.many1 $ R.try read_tag_path_section
475 read_tag_path_section :: Stream s m Char => ParsecT s u m Tag.Section
476 read_tag_path_section = do
478 ((R.many1 $ read_tag_path_section_char) <* R.char read_tag_value_sep)
480 read_tag_value :: Stream s m Char => ParsecT s u m Tag.Value
483 R.manyTill R.anyChar $ do
485 R.try (R.char read_tag_sep
486 >> R.many R.space_horizontal
487 >> void read_tag_path_section)
488 <|> R.try (void (R.try R.new_line))
495 => ParsecT s u m (Map Tag.Path [Tag.Value])
497 Map.fromListWith (flip mappend)
498 . List.map (\(p, v) -> (p, [v])) <$> do
499 R.many_separated read_tag $ do
500 _ <- R.char read_tag_sep
503 read_not_tag :: Stream s m Char => ParsecT s u m [Char]
507 R.satisfy (\c -> c /= read_tag_value_sep && not (Char.isSpace c))
515 , Stream s (R.Error_State Read_Error m) Char
516 ) => ParsecT s (Read_Context c j)
517 (R.Error_State Read_Error m)
518 (Posting_Typed Posting)
520 posting_sourcepos <- R.getPosition
521 R.skipMany1 $ R.space_horizontal
522 posting_status <- read_status
525 let Posting_Typed type_ posting_account = read_posting_type acct
529 (void R.tab <|> void (R.count 2 R.space_horizontal))
532 R.many_separated read_amount $ do
534 _ <- R.char read_amount_sep
536 ctx <- flip liftM R.getState $ \ctx ->
538 { read_context_journal=
539 let jnl = read_context_journal ctx in
541 { journal_amount_styles =
543 (\(Amount_Styles styles) (style, amt) ->
545 Map.insertWith (flip mappend) -- NOTE: prefer first style
546 (Amount.amount_unit amt)
548 (journal_amount_styles jnl)
554 Map.fromListWith Quantity.quantity_add $
557 let unit = Amount.amount_unit amt in
558 ( if unit == Unit.unit_empty
559 then maybe unit id (read_context_unit ctx)
561 , Amount.amount_quantity amt
568 -- TODO: balance assertion
570 posting_comments <- read_comments
571 let posting_tags@(Tags tags_) =
572 tags_of_comments posting_comments
575 case Map.lookup ("date":|[]) tags_ of
578 let date2s = Map.lookup ("date2":|[]) tags_ -- NOTE: support hledger's date2
580 forM (dates `mappend` fromMaybe [] date2s) $ \s ->
581 R.runParserT_with_Error_fail "tag date" id
582 (read_date Read_Error_date (Just $ read_context_year ctx) <* R.eof) ()
584 >>= \dates_ -> case (dates, date2s) of -- NOTE: put hledger's date2 at least in second position
586 return $ read_context_date ctx:dates_
588 return $ Posting_Typed type_ Posting
595 , posting_tags = Posting_Tags posting_tags
599 read_amount_sep :: Char
600 read_amount_sep = '+'
602 tags_of_comments :: [Comment] -> Tags
605 Map.unionsWith mappend
607 ( Data.Either.either (const Map.empty) id
608 . R.runParser (read_not_tag >> read_tags <* R.eof) () "" )
610 comments_without_tags :: [Comment] -> [Comment]
611 comments_without_tags =
614 Data.Either.either (const c) Text.pack $
616 (read_not_tag <* read_tags <* R.eof)
620 read_status :: Stream s m Char => ParsecT s u m Status
624 _ <- (R.char '*' <|> R.char '!')
629 -- | Return the 'Posting_Type' and stripped 'Account' of the given 'Account'.
630 read_posting_type :: Account -> (Posting_Typed Account)
631 read_posting_type acct =
632 fromMaybe (Posting_Typed Posting_Type_Regular acct) $ do
635 case Text.stripPrefix virtual_begin name of
637 name'' <- liftM Text.strip $ Text.stripSuffix virtual_end name'
638 guard $ not $ Text.null name''
639 Just $ Posting_Typed Posting_Type_Virtual $ name'':|[]
641 name' <- liftM Text.strip $
642 Text.stripPrefix virtual_balanced_begin name
643 >>= Text.stripSuffix virtual_balanced_end
644 guard $ not $ Text.null name'
645 Just $ Posting_Typed Posting_Type_Virtual_Balanced $ name':|[]
646 first_name:|acct' -> do
647 let rev_acct' = List.reverse acct'
648 let last_name = List.head rev_acct'
649 case liftM Text.stripStart $
650 Text.stripPrefix virtual_begin first_name of
651 Just first_name' -> do
652 last_name' <- liftM Text.stripEnd $
653 Text.stripSuffix virtual_end last_name
654 guard $ not $ Text.null first_name'
655 guard $ not $ Text.null last_name'
657 Posting_Type_Virtual $
658 first_name':| List.reverse (last_name':List.tail rev_acct')
660 first_name' <- liftM Text.stripStart $
661 Text.stripPrefix virtual_balanced_begin first_name
662 last_name' <- liftM Text.stripEnd $
663 Text.stripSuffix virtual_balanced_end last_name
664 guard $ not $ Text.null first_name'
665 guard $ not $ Text.null last_name'
667 Posting_Type_Virtual_Balanced $
668 first_name':|List.reverse (last_name':List.tail rev_acct')
670 virtual_begin = Text.singleton read_posting_type_virtual_begin
671 virtual_end = Text.singleton read_posting_type_virtual_end
672 virtual_balanced_begin = Text.singleton read_posting_type_virtual_balanced_begin
673 virtual_balanced_end = Text.singleton read_posting_type_virtual_balanced_end
675 read_posting_type_virtual_begin :: Char
676 read_posting_type_virtual_begin = '('
677 read_posting_type_virtual_balanced_begin :: Char
678 read_posting_type_virtual_balanced_begin = '['
679 read_posting_type_virtual_end :: Char
680 read_posting_type_virtual_end = ')'
681 read_posting_type_virtual_balanced_end :: Char
682 read_posting_type_virtual_balanced_end = ']'
684 -- * Read 'Transaction'
689 , Stream s (R.Error_State Read_Error m) Char
690 ) => ParsecT s (Read_Context c j)
691 (R.Error_State Read_Error m)
693 read_transaction = (do
695 transaction_sourcepos <- R.getPosition
696 transaction_comments_before <-
700 _ -> return x <* R.new_line
701 date_ <- read_date Read_Error_date (Just $ read_context_year ctx)
703 R.option [] $ R.try $ do
705 _ <- R.char read_date_sep
708 (read_date Read_Error_date (Just $ read_context_year ctx)) $
710 R.many $ R.space_horizontal
711 >> R.char read_date_sep
712 >> (R.many $ R.space_horizontal)
713 let transaction_dates = (date_, dates_)
715 transaction_status <- read_status
716 transaction_code <- R.option "" $ R.try read_code
718 transaction_wording <- read_wording
720 transaction_comments_after <- read_comments
721 let transaction_tags =
724 (tags_of_comments transaction_comments_before)
725 (tags_of_comments transaction_comments_after)
727 (postings_unchecked, postings_not_regular) <-
728 first (postings_by_account . List.map
729 (\(Posting_Typed _ p) -> p)) .
730 List.partition (\(Posting_Typed pt _) ->
731 Posting_Type_Regular == pt) <$>
732 R.many1_separated read_posting R.new_line
733 let (transaction_virtual_postings, balanced_virtual_postings_unchecked) =
734 join (***) (postings_by_account . List.map
735 (\(Posting_Typed _ p) -> p)) $
736 List.partition (\(Posting_Typed pt _) ->
737 Posting_Type_Virtual == pt)
742 , transaction_comments_before
743 , transaction_comments_after
745 , transaction_wording
746 , transaction_postings=postings_unchecked
747 , transaction_sourcepos
751 let styles = journal_amount_styles $ read_context_journal ctx
752 transaction_postings <-
753 case Balance.infer_equilibrium postings_unchecked of
754 (_, Left ko) -> R.fail_with "transaction infer_equilibrium" $
755 Read_Error_transaction_not_equilibrated styles tr_unchecked ko
756 (_bal, Right ok) -> return ok
757 transaction_balanced_virtual_postings <-
758 case Balance.infer_equilibrium balanced_virtual_postings_unchecked of
759 (_, Left ko) -> R.fail_with "transaction infer_equilibrium" $
760 Read_Error_virtual_transaction_not_equilibrated styles tr_unchecked ko
761 (_bal, Right ok) -> return ok
764 { transaction_postings =
765 Map.unionsWith mappend
766 [ transaction_postings
767 , fmap (fmap set_virtual_tag) transaction_virtual_postings
768 , fmap (fmap set_virtual_tag) transaction_balanced_virtual_postings
773 set_virtual_tag :: Posting -> Posting
775 p@Posting{posting_tags=Posting_Tags (Tags attrs)} =
776 p{posting_tags = Posting_Tags $ Tags $ Map.insert ("Virtual":|[]) [] attrs}
778 read_date_sep :: Char
784 => ParsecT s (Read_Context c j) m Code
788 R.between (R.char '(') (R.char ')') $
789 R.many $ R.satisfy (\c -> c /= ')' && not (R.is_space_horizontal c))
794 => ParsecT s u m Wording
797 R.many $ R.try description_char
800 description_char :: Stream s m Char => ParsecT s u m Char
801 description_char = do
804 _ | c == read_comment_begin -> R.parserZero
805 _ | R.is_space_horizontal c -> return c <* (R.lookAhead $ R.try $ description_char)
806 _ | not (Char.isSpace c) -> return c
812 :: (Consable c j, Stream s m Char)
813 => ParsecT s (Read_Context c j) m ()
814 read_directive_alias = do
815 _ <- R.string "alias"
816 R.skipMany1 $ R.space_horizontal
817 pattern <- read_account_pattern
824 Account_Pattern_Exact acct ->
825 R.modifyState $ \ctx -> ctx{read_context_aliases_exact=
826 Map.insert acct repl $ read_context_aliases_exact ctx}
827 Account_Pattern_Joker jokr ->
828 R.modifyState $ \ctx -> ctx{read_context_aliases_joker=
829 (jokr, repl):read_context_aliases_joker ctx}
830 Account_Pattern_Regex regx ->
831 R.modifyState $ \ctx -> ctx{read_context_aliases_regex=
832 (regx, repl):read_context_aliases_regex ctx}
836 :: (Consable c j, Stream s m Char)
837 => ParsecT s (Read_Context c j) m ()
838 read_default_year = (do
839 year <- R.integer_of_digits 10 <$> R.many1 R.digit
841 read_context_ <- R.getState
842 R.setState read_context_{read_context_year=year}
845 read_default_unit_and_style
848 => ParsecT s (Read_Context c j) m ()
849 read_default_unit_and_style = (do
850 (sty, amt) <- read_amount
853 let unit = Amount.amount_unit amt
855 { read_context_journal =
856 let jnl = read_context_journal ctx in
858 { journal_amount_styles =
859 let Amount_Styles styles =
860 journal_amount_styles jnl in
862 Map.insertWith const unit sty styles
864 , read_context_unit = Just unit
866 ) <?> "default unit and style"
871 , Stream s (R.Error_State Read_Error IO) Char
872 ) => ParsecT s (Read_Context c j) (R.Error_State Read_Error IO) ()
874 sourcepos <- R.getPosition
875 filename <- R.manyTill R.anyChar (R.lookAhead (R.try R.new_line <|> R.eof))
876 read_context_including <- R.getState
877 let journal_including = read_context_journal read_context_including
878 let cwd = Path.takeDirectory (R.sourceName sourcepos)
879 journal_file <- liftIO $ Path.abs cwd filename
881 join $ liftIO $ Exception.catch
882 (liftM return $ Text.IO.readFile journal_file)
883 (return . R.fail_with "include reading" . Read_Error_reading_file journal_file)
884 (journal_included, read_context_included) <- do
886 R.runParserT_with_Error
887 (R.and_state $ read_journal_rec journal_file)
888 read_context_including
889 { read_context_journal=
891 { journal_chart = journal_chart journal_including
892 , journal_amount_styles = journal_amount_styles journal_including
897 Right ok -> return ok
898 Left ko -> R.fail_with "include parsing" (Read_Error_including_file journal_file ko)
900 read_context_included
901 { read_context_journal=
904 journal_included{journal_files=[journal_file]} :
905 journal_includes journal_including
907 journal_chart journal_included
908 , journal_amount_styles=
909 journal_amount_styles journal_included
918 , Stream s (R.Error_State Read_Error IO) Char
919 ) => ParsecT s (Read_Context c j) (R.Error_State Read_Error IO) ()
921 -- sourcepos <- R.getPosition
926 tags_ <- R.many_separated
927 (R.try (R.skipMany1 R.space_horizontal >> read_tag
928 <* read_hspaces <* read_comments))
932 TreeMap.singleton acct $
938 (flip (\(p:|ps, v) ->
939 TreeMap.insert mappend
940 (p:|ps `mappend` [v])
946 let j = read_context_journal ctx
948 ctx{read_context_journal=
953 { Chart.chart_accounts
954 , Chart.chart_anchors = mempty
955 -- , Chart.chart_tags
966 , Stream s (R.Error_State Read_Error IO) Char
968 -> ParsecT s (Read_Context c j)
969 (R.Error_State Read_Error IO)
971 read_journal filepath = (do
972 currentLocalTime <- liftIO $
974 <$> Time.getCurrentTimeZone
975 <*> Time.getCurrentTime
976 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
978 R.setState $ ctx{read_context_year=currentLocalYear}
979 read_journal_rec filepath
985 , Stream s (R.Error_State Read_Error IO) Char
988 -> ParsecT s (Read_Context c j)
989 (R.Error_State Read_Error IO)
991 read_journal_rec journal_file = do
992 last_read_time <- liftIO Date.now
1000 journal_ <- read_context_journal <$> R.getState
1003 { journal_files = [journal_file]
1004 , journal_includes = List.reverse $ journal_includes journal_
1005 , journal_last_read_time = last_read_time
1010 => ParsecT s u m (ParsecT s u m ())
1013 R.skipMany (read_hspaces >> R.new_line)
1015 R.skipMany (read_hspaces >> R.new_line)
1016 R.try (read_hspaces >> R.eof) <|> loop r
1020 , u ~ Read_Context c j
1021 , m ~ R.Error_State Read_Error IO
1023 => ParsecT s u m (ParsecT s u m ())
1026 _ <- R.lookAhead (R.try $ R.char read_comment_begin)
1028 _cmts <- read_comments
1030 R.modifyState $ \ctx ->
1031 let j = read_context_journal ctx in
1032 ctx{read_context_journal=
1034 mcons (read_context_filter ctx) cmts $
1042 , u ~ Read_Context c j
1043 , m ~ R.Error_State Read_Error IO
1045 => ParsecT s u m (ParsecT s u m ())
1047 let choice s = R.string s >> R.skipMany1 R.space_horizontal
1049 [ choice "Y" >> return read_default_year
1050 , choice "D" >> return read_default_unit_and_style
1051 , choice "!include" >> return read_include
1056 , u ~ Read_Context c j
1057 , m ~ R.Error_State Read_Error IO
1059 => ParsecT s u m (ParsecT s u m ())
1060 jump_transaction = do
1061 _ <- R.lookAhead $ R.try (R.many1 R.digit >> read_date_separator)
1063 t <- read_transaction
1064 R.modifyState $ \ctx ->
1065 let j = read_context_journal ctx in
1066 ctx{read_context_journal=
1069 (read_context_cons ctx $
1070 Chart.Charted (journal_chart j) t)
1071 (journal_content j)}}
1075 , u ~ Read_Context c j
1076 , m ~ R.Error_State Read_Error IO
1078 => ParsecT s u m (ParsecT s u m ())
1085 :: (Consable c j, Monoid j)
1088 -> ExceptT [R.Error Read_Error] IO (Journal j)
1092 (liftM Right $ Text.IO.readFile path) $
1093 \ko -> return $ Left $
1094 [R.Error_Custom (R.initialPos path) $
1095 Read_Error_reading_file path ko]
1096 >>= liftIO . R.runParserT_with_Error
1097 (read_journal path) ctx path
1099 Left ko -> throwE $ ko
1100 Right ok -> ExceptT $ return $ Right ok