1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE NamedFieldPuns #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE ScopedTypeVariables #-}
6 {-# LANGUAGE TupleSections #-}
7 {-# LANGUAGE TypeFamilies #-}
8 module Hcompta.Ledger.Read where
10 import Prelude (Int, Integer, Num(..), fromIntegral)
11 import Control.Applicative (Applicative(..))
12 import Control.Arrow ((***), first)
14 import Data.Char (Char)
15 import qualified Data.Char as Char
17 import Data.Either (Either(..), either)
18 import Data.Eq (Eq(..))
19 import qualified Control.Exception.Safe as Exn
20 import qualified System.FilePath.Posix as FilePath
21 import Data.Function (($), (.), id, const, flip)
22 import Data.Functor ((<$>))
23 import System.IO (IO, FilePath)
24 import qualified Data.List as List
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(..), fromMaybe, maybe)
30 import Control.Monad (Monad(..), forM, guard, join, void)
31 import Control.Monad.IO.Class (liftIO)
32 import Control.Monad.Trans.Except (ExceptT(..), throwE)
33 import Data.Monoid (Monoid(..))
34 import Data.Ord (Ord(..))
35 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
36 import Data.String (String, fromString)
37 import Data.Text (Text)
38 import qualified Data.Text as Text
39 import qualified Data.Text.IO as Text.IO (readFile)
40 import qualified Data.Time.Calendar as Time
41 import qualified Data.Time.Clock as Time
42 import Data.Time.LocalTime (TimeZone(..))
43 import qualified Data.Time.LocalTime as Time
44 import Data.Typeable ()
45 import qualified Text.Parsec as R hiding
58 import qualified Text.Parsec.Combinator.CorrectSourcePosWithTab as R
59 import qualified Text.Parsec.Error.Custom as R
60 import qualified Text.Parsec.Pos as R
61 import Text.Regex.TDFA (Regex)
62 import qualified Text.Regex.TDFA.Replace.Text as Regex
63 import Text.Show (Show)
65 import qualified Hcompta as H
66 import Hcompta.Lib.Consable (Consable(..))
67 import qualified Data.TreeMap.Strict as TreeMap
69 import Hcompta.Ledger.Account
70 import Hcompta.Ledger.Amount
71 import Hcompta.Ledger.Chart
72 import Hcompta.Ledger.Posting
73 import Hcompta.Ledger.Transaction
74 import Hcompta.Ledger.Journal
75 import qualified Hcompta.Ledger.Lib.Parsec as R
76 import qualified Hcompta.Ledger.Lib.FilePath as FilePath
78 -- * Type 'Context_Read'
82 { context_read_account_prefix :: !(Maybe Account)
83 , context_read_aliases_exact :: !(Map Account Account)
84 , context_read_aliases_joker :: ![(Account_Joker, Account)]
85 , context_read_aliases_regex :: ![(Regex, Account)]
86 , context_read_cons :: Charted Transaction -> c
87 , context_read_date :: !H.Date
88 , context_read_journal :: !(Journal j)
89 , context_read_unit :: !(Maybe Unit)
90 , context_read_year :: !H.Year
95 => (Charted Transaction -> c)
98 context_read context_read_cons context_read_journal =
100 { context_read_account_prefix = Nothing
101 , context_read_aliases_exact = mempty
102 , context_read_aliases_joker = []
103 , context_read_aliases_regex = []
105 , context_read_date = H.date_epoch
106 , context_read_journal
107 , context_read_unit = Nothing
108 , context_read_year = H.date_year H.date_epoch
111 -- * Type 'Error_Read'
114 = Error_Read_date Error_Read_Date
115 | Error_Read_transaction_not_equilibrated
119 , H.Balance_by_Unit_Sum Account_Section
120 (H.Polarized Quantity)
122 | Error_Read_virtual_transaction_not_equilibrated
126 , H.Balance_by_Unit_Sum Account_Section
127 (H.Polarized Quantity)
129 | Error_Read_reading_file FilePath Exn.IOException
130 | Error_Read_including_file FilePath [R.Error Error_Read]
133 -- * Read common patterns
135 read_hspaces :: Stream s m Char => ParsecT s u m ()
136 read_hspaces = R.skipMany R.spaceHorizontal
140 read_account :: Stream s m Char => ParsecT s u m Account
142 R.notFollowedBy $ R.spaceHorizontal
143 (H.account_from_List <$>) $
144 R.many1_separated read_account_section $
145 R.char read_account_section_sep
147 read_account_section :: Stream s m Char => ParsecT s u m Text
148 read_account_section =
150 R.many1 (R.try account_name_char)
152 account_name_char :: Stream s m Char => ParsecT s u m Char
153 account_name_char = do
156 _ | c == read_comment_prefix -> R.parserZero
157 _ | c == read_account_section_sep -> R.parserZero
158 _ | c /= '\t' && R.isSpaceHorizontal c -> do
159 _ <- R.notFollowedBy $ R.spaceHorizontal
160 return c <* R.lookAhead (R.try $
161 R.try (R.char read_account_section_sep) <|>
164 _ | not (Char.isSpace c) -> return c
167 read_account_section_sep :: Char
168 read_account_section_sep = ':'
170 read_comment_prefix :: Char
171 read_comment_prefix = ';'
173 read_account_section_joker
175 => ParsecT s u m Account_Joker_Section
176 read_account_section_joker = do
177 n <- R.option Nothing $ (Just <$> read_account_section)
179 Nothing -> R.char read_account_section_sep >> return Account_Joker_Any
180 Just n' -> return $ Account_Joker_Section n'
184 => ParsecT s u m Account_Joker
185 read_account_joker = do
186 R.notFollowedBy $ R.spaceHorizontal
187 R.many1_separated read_account_section_joker $ R.char read_account_section_sep
191 => ParsecT s u m Regex
192 read_account_regex = do
193 re <- R.many1 $ R.satisfy (not . R.isSpaceHorizontal)
198 => ParsecT s u m Account_Pattern
199 read_account_pattern =
201 [ Account_Pattern_Exact <$> (R.char '=' >> read_account)
202 , Account_Pattern_Joker <$> (R.char '*' >> read_account_joker)
203 , Account_Pattern_Regex <$> (R.option '~' (R.char '~') >> read_account_regex)
210 => Char -- ^ Integral grouping separator.
211 -> Char -- ^ Fractioning separator.
212 -> Char -- ^ Fractional grouping separator.
214 ( [String] -- integral
215 , [String] -- fractional
216 , Maybe Amount_Style_Fractioning -- fractioning
217 , Maybe Amount_Style_Grouping -- grouping_integral
218 , Maybe Amount_Style_Grouping -- grouping_fractional
220 read_quantity int_group_sep frac_sep frac_group_sep = do
221 (integral, grouping_integral) <- do
224 [] -> return ([], Nothing)
226 t <- R.many $ R.char int_group_sep >> R.many1 R.digit
228 return (digits, grouping_of_digits int_group_sep digits)
229 (fractional, fractioning, grouping_fractional) <-
232 _ -> R.option ([], Nothing, Nothing)) $ do
233 fractioning <- R.char frac_sep
235 t <- R.many $ R.char frac_group_sep >> R.many1 R.digit
237 return (digits, Just fractioning
238 , grouping_of_digits frac_group_sep $ List.reverse digits)
244 , grouping_fractional
247 grouping_of_digits :: Char -> [String] -> Maybe Amount_Style_Grouping
248 grouping_of_digits group_sep digits =
253 Amount_Style_Grouping group_sep $
254 canonicalize_grouping $
255 List.map List.length $ digits
256 canonicalize_grouping :: [Int] -> [Int]
257 canonicalize_grouping groups =
258 List.foldl' -- NOTE: remove duplicates at beginning and reverse.
259 (\acc l0 -> case acc of
260 l1:_ -> if l0 == l1 then acc else l0:acc
262 case groups of -- NOTE: keep only longer at beginning.
263 l0:l1:t -> if l0 > l1 then groups else l1:t
268 read_unit :: Stream s m Char => ParsecT s u m Unit
269 read_unit = (<?> "unit") $
272 unquoted :: Stream s m Char => ParsecT s u m Unit
277 case Char.generalCategory c of
278 Char.CurrencySymbol -> True
279 Char.LowercaseLetter -> True
280 Char.ModifierLetter -> True
281 Char.OtherLetter -> True
282 Char.TitlecaseLetter -> True
283 Char.UppercaseLetter -> True
285 quoted :: Stream s m Char => ParsecT s u m Unit
288 R.between (R.char '"') (R.char '"') $
296 => ParsecT s u m (Amount_Styled Amount)
298 left_signing <- read_sign
300 R.option Nothing $ do
302 s <- R.many $ R.spaceHorizontal
303 return $ Just $ (u, not $ List.null s)
306 ( amount_style_integral
307 , amount_style_fractional
308 , amount_style_fractioning
309 , amount_style_grouping_integral
310 , amount_style_grouping_fractional
311 ) <- (<?> "quantity") $
313 [ read_quantity '_' ',' '_' <* R.notFollowedBy (R.oneOf ",._")
314 , read_quantity '_' '.' '_' <* R.notFollowedBy (R.oneOf ",._")
315 , read_quantity ',' '.' '_' <* R.notFollowedBy (R.oneOf ",._")
316 , read_quantity '.' ',' '_' <* R.notFollowedBy (R.oneOf ",._")
318 let int = List.concat amount_style_integral
319 let frac = List.concat amount_style_fractional
320 let precision = List.length frac
321 guard (precision <= 255)
322 let mantissa = R.integer_of_digits 10 $ int `mappend` frac
325 (fromIntegral precision)
328 { amount_style_fractioning
329 , amount_style_grouping_integral
330 , amount_style_grouping_fractional
334 , amount_style_unit_side
335 , amount_style_unit_spaced ) <-
338 return (u, Just Amount_Style_Side_Left, Just s)
340 R.option (H.unit_empty, Nothing, Nothing) $ R.try $ do
341 s <- R.many R.spaceHorizontal
345 , Just Amount_Style_Side_Right
346 , Just $ not $ List.null s )
349 { amount_style_unit_side
350 , amount_style_unit_spaced
353 { amount_quantity = left_signing qty
358 -- | Parse either '-' into 'negate', or '+' or '' into 'id'.
359 read_sign :: (Stream s m Char, Num i) => ParsecT s u m (i -> i)
361 (R.char '-' >> return negate)
362 <|> (R.char '+' >> return id)
368 = Error_Read_Date_year_or_day_is_missing
369 | Error_Read_Date_invalid_date (Integer, Int, Int)
370 | Error_Read_Date_invalid_time_of_day (Int, Int, Integer)
373 -- | Read a 'Date' in @[YYYY[/-]]MM[/-]DD[_HH:MM[:SS][TZ]]@ format.
375 :: (Stream s (R.State_Error e m) Char, Monad m)
376 => (Error_Read_Date -> e) -> Maybe Integer
377 -> ParsecT s u (R.State_Error e m) H.Date
378 read_date err def_year = (<?> "date") $ do
379 let read_2_or_1_digits = R.try (R.count 2 R.digit) <|> R.count 1 R.digit
380 n0 <- R.many1 R.digit
381 day_sep <- R.char read_date_ymd_sep
382 n1 <- read_2_or_1_digits
383 n2 <- R.option Nothing $ R.try $ do
385 Just <$> read_2_or_1_digits
387 case (n2, def_year) of
388 (Nothing, Nothing) -> R.parserFailWith "date" $
389 err Error_Read_Date_year_or_day_is_missing
390 (Nothing, Just year) -> return (year, n0, n1)
391 (Just d, _) -> return (R.integer_of_digits 10 n0, n1, d)
392 let month = fromInteger $ R.integer_of_digits 10 m
393 let dom = fromInteger $ R.integer_of_digits 10 d
394 day <- case Time.fromGregorianValid year month dom of
395 Nothing -> R.parserFailWith "date" $
396 err $ Error_Read_Date_invalid_date (year, month, dom)
397 Just day -> return day
398 (hour, minu, sec, tz) <-
399 R.option (0, 0, 0, Time.utc) $ R.try $ do
401 hour <- read_2_or_1_digits
402 sep <- R.char read_hour_separator
403 minu <- read_2_or_1_digits
404 sec <- R.option Nothing $ R.try $ do
406 Just <$> read_2_or_1_digits
407 tz <- R.option Time.utc $ R.try $
410 ( fromInteger $ R.integer_of_digits 10 hour
411 , fromInteger $ R.integer_of_digits 10 minu
412 , maybe 0 (R.integer_of_digits 10) sec
414 tod <- case Time.makeTimeOfDayValid hour minu (fromInteger sec) of
415 Nothing -> R.parserFailWith "date" $
416 err $ Error_Read_Date_invalid_time_of_day (hour, minu, sec)
417 Just tod -> return tod
418 return $ Time.localTimeToUTC tz (Time.LocalTime day tod)
420 -- | Separator for year, month and day: "-".
421 read_date_ymd_sep :: Char
422 read_date_ymd_sep = '-'
424 -- | Separator for hour, minute and second: ":".
425 read_hour_separator :: Char
426 read_hour_separator = ':'
428 read_time_zone :: Stream s m Char => ParsecT s u m TimeZone
430 -- DOC: http://www.timeanddate.com/time/zones/
431 -- TODO: only a few time zones are suported below.
432 -- TODO: check the timeZoneSummerOnly values
436 [ R.char 'A' >> R.choice
437 [ R.string "ST" >> return (TimeZone ((-4) * 60) False "AST")
438 , R.string "DT" >> return (TimeZone ((-3) * 60) True "ADT")
439 , return (TimeZone ((-1) * 60) False "A")
441 , R.char 'B' >> R.choice
442 [ R.string "ST" >> return (TimeZone ((-11) * 60) False "BST")
443 , R.string "DT" >> return (TimeZone ((-10) * 60) True "BDT")
445 , R.char 'C' >> R.choice
446 [ R.char 'E' >> R.choice
447 [ R.string "T" >> return (TimeZone ((1) * 60) True "CET")
448 , R.string "ST" >> return (TimeZone ((2) * 60) False "CEST")
450 , R.string "ST" >> return (TimeZone ((-6) * 60) False "CST")
451 , R.string "DT" >> return (TimeZone ((-5) * 60) True "CDT")
453 , R.char 'E' >> R.choice
454 [ R.string "ST" >> return (TimeZone ((-5) * 60) False "EST")
455 , R.string "DT" >> return (TimeZone ((-4) * 60) True "EDT")
457 , R.string "GMT" >> return (TimeZone 0 False "GMT")
458 , R.char 'H' >> R.choice
459 [ R.string "ST" >> return (TimeZone ((-10) * 60) False "HST")
460 , R.string "DT" >> return (TimeZone (( -9) * 60) True "HDT")
462 , R.char 'M' >> R.choice
463 [ R.string "ST" >> return (TimeZone ((-7) * 60) False "MST")
464 , R.string "DT" >> return (TimeZone ((-6) * 60) True "MDT")
465 , return (TimeZone ((-12) * 60) False "M")
467 , R.char 'N' >> R.choice
468 [ R.string "ST" >> return (TimeZone ((-3) * 60 - 30) False "NST")
469 , return (TimeZone (1 * 60) False "N")
471 , R.char 'P' >> R.choice
472 [ R.string "ST" >> return (TimeZone ((-8) * 60) False "PST")
473 , R.string "DT" >> return (TimeZone ((-7) * 60) True "PDT")
475 , R.char 'Y' >> R.choice
476 [ R.string "ST" >> return (TimeZone ((-9) * 60) False "YST")
477 , R.string "DT" >> return (TimeZone ((-8) * 60) True "YDT")
478 , return (TimeZone (12 * 60) False "Y")
480 , R.char 'Z' >> return (TimeZone 0 False "Z")
482 , read_time_zone_digits
485 read_time_zone_digits :: Stream s m Char => ParsecT s u m TimeZone
486 read_time_zone_digits = do
488 hour <- R.integer_of_digits 10 <$> R.count 2 R.digit
492 R.integer_of_digits 10 <$> R.count 2 R.digit
494 { timeZoneMinutes = sign_ (fromInteger hour * 60 + fromInteger minute)
495 , timeZoneSummerOnly = False
496 , timeZoneName = Time.timeZoneOffsetString tz
504 => ParsecT s u m Comment
505 read_comment = (<?> "comment") $ do
506 _ <- R.char read_comment_prefix
508 R.manyTill R.anyChar (R.lookAhead (R.try R.newline <|> R.eof))
510 -- ** Read 'Comment's
514 => ParsecT s u m [Comment]
515 read_comments = (<?> "comments") $
518 R.many1_separated read_comment
519 (R.newline >> read_hspaces))
524 read_tag_value_sep :: Char
525 read_tag_value_sep = ':'
530 read_tag_path_section_char
532 => ParsecT s u m Char
533 read_tag_path_section_char =
535 c /= read_tag_value_sep &&
539 read_tag :: Stream s m Char => ParsecT s u m H.Tag
540 read_tag = (<?> "tag") $
545 read_tag_path :: Stream s m Char => ParsecT s u m H.Tag_Path
547 (NonEmpty.fromList <$>) $
548 R.many1 $ R.try read_tag_path_section
550 read_tag_path_section :: Stream s m Char => ParsecT s u m H.Tag_Section
551 read_tag_path_section =
553 (R.many1 read_tag_path_section_char <* R.char read_tag_value_sep)
555 read_tag_value :: Stream s m Char => ParsecT s u m H.Tag_Value
558 R.manyTill R.anyChar $
560 R.try (R.char read_tag_sep
561 >> R.many R.spaceHorizontal
562 >> void read_tag_path_section)
563 <|> R.try (void (R.try R.newline))
570 => ParsecT s u m (Map H.Tag_Path [H.Tag_Value])
572 (Map.fromListWith (flip mappend) .
573 List.map (\(p, v) -> (p, [v])) <$>) $
574 R.many_separated read_tag $ do
575 _ <- R.char read_tag_sep
578 read_not_tag :: Stream s m Char => ParsecT s u m [Char]
582 R.satisfy (\c -> c /= read_tag_value_sep && not (Char.isSpace c))
590 , Stream s (R.State_Error Error_Read m) Char
591 ) => ParsecT s (Context_Read c j)
592 (R.State_Error Error_Read m)
593 (Posting_Typed Posting)
594 read_posting = (<?> "posting") $ do
595 posting_sourcepos <- R.getPosition
596 R.skipMany1 $ R.spaceHorizontal
597 posting_status <- read_status
600 let Posting_Typed type_ posting_account = read_posting_type acct
601 posting_amounts <- (<?> "amounts") $
604 (void R.tab <|> void (R.count 2 R.spaceHorizontal))
607 R.many_separated read_amount $ do
609 _ <- R.char read_amount_sep
611 ctx <- (<$> R.getState) $ \ctx ->
613 { context_read_journal=
614 let jnl = context_read_journal ctx in
616 { journal_amount_styles =
618 (\(Amount_Styles styles) (style, amt) ->
620 Map.insertWith (flip mappend) -- NOTE: prefer first style
623 (journal_amount_styles jnl)
629 Map.fromListWith H.quantity_add $
632 let unit = H.amount_unit amt in
633 ( if unit == H.unit_empty
634 then fromMaybe unit $ context_read_unit ctx
636 , H.amount_quantity amt
643 -- TODO: balance assertion
645 posting_comments <- read_comments
646 let posting_tags@(H.Tags tags_) =
647 tags_of_comments posting_comments
650 case Map.lookup ("date":|[]) tags_ of
653 let date2s = Map.lookup ("date2":|[]) tags_ -- NOTE: support hledger's date2
654 dates_ <- forM (dates `mappend` fromMaybe [] date2s) $ \s ->
655 R.runParserTWithErrorPropagation "tag date" id
656 (read_date Error_Read_date (Just $ context_read_year ctx) <* R.eof) ()
658 case (dates, date2s) of
659 -- NOTE: put hledger's date2 at least in second position
661 return $ context_read_date ctx:dates_
663 return $ Posting_Typed type_ Posting
670 , posting_tags = H.Posting_Tags posting_tags
673 read_amount_sep :: Char
674 read_amount_sep = '+'
676 tags_of_comments :: [Comment] -> H.Tags
679 Map.unionsWith mappend
681 ( Data.Either.either (const Map.empty) id
682 . R.runParser (read_not_tag >> read_tags <* R.eof) () "" )
684 comments_without_tags :: [Comment] -> [Comment]
685 comments_without_tags =
688 Data.Either.either (const c) Text.pack $
690 (read_not_tag <* read_tags <* R.eof)
694 read_status :: Stream s m Char => ParsecT s u m Status
695 read_status = (<?> "status") $
698 _ <- (R.char '*' <|> R.char '!')
702 -- | Return the 'Posting_Type' and stripped 'Account' of the given 'Account'.
703 read_posting_type :: Account -> (Posting_Typed Account)
704 read_posting_type acct =
705 fromMaybe (Posting_Typed Posting_Type_Regular acct) $
708 case Text.stripPrefix virtual_begin name of
710 name'' <- Text.strip <$> Text.stripSuffix virtual_end name'
711 guard $ not $ Text.null name''
712 Just $ Posting_Typed Posting_Type_Virtual $ name'':|[]
714 name' <- Text.strip <$>
715 Text.stripPrefix virtual_balanced_begin name
716 >>= Text.stripSuffix virtual_balanced_end
717 guard $ not $ Text.null name'
718 Just $ Posting_Typed Posting_Type_Virtual_Balanced $ name':|[]
719 first_name:|acct' -> do
720 let rev_acct' = List.reverse acct'
721 let last_name = List.head rev_acct'
722 case Text.stripStart <$>
723 Text.stripPrefix virtual_begin first_name of
724 Just first_name' -> do
725 last_name' <- Text.stripEnd <$>
726 Text.stripSuffix virtual_end last_name
727 guard $ not $ Text.null first_name'
728 guard $ not $ Text.null last_name'
730 Posting_Type_Virtual $
731 first_name':| List.reverse (last_name':List.tail rev_acct')
733 first_name' <- Text.stripStart <$>
734 Text.stripPrefix virtual_balanced_begin first_name
735 last_name' <- Text.stripEnd <$>
736 Text.stripSuffix virtual_balanced_end last_name
737 guard $ not $ Text.null first_name'
738 guard $ not $ Text.null last_name'
740 Posting_Type_Virtual_Balanced $
741 first_name':|List.reverse (last_name':List.tail rev_acct')
743 virtual_begin = Text.singleton read_posting_type_virtual_begin
744 virtual_end = Text.singleton read_posting_type_virtual_end
745 virtual_balanced_begin = Text.singleton read_posting_type_virtual_balanced_begin
746 virtual_balanced_end = Text.singleton read_posting_type_virtual_balanced_end
748 read_posting_type_virtual_begin :: Char
749 read_posting_type_virtual_begin = '('
750 read_posting_type_virtual_balanced_begin :: Char
751 read_posting_type_virtual_balanced_begin = '['
752 read_posting_type_virtual_end :: Char
753 read_posting_type_virtual_end = ')'
754 read_posting_type_virtual_balanced_end :: Char
755 read_posting_type_virtual_balanced_end = ']'
757 -- * Read 'Transaction'
762 , Stream s (R.State_Error Error_Read m) Char
763 ) => ParsecT s (Context_Read c j)
764 (R.State_Error Error_Read m)
766 read_transaction = (<?> "transaction") $ do
768 transaction_sourcepos <- R.getPosition
769 transaction_comments_before <- do
770 cmts <- read_comments
773 _ -> return cmts <* R.newline
774 date_ <- read_date Error_Read_date (Just $ context_read_year ctx)
776 R.option [] $ R.try $ do
778 _ <- R.char read_transaction_date_sep
781 (read_date Error_Read_date (Just $ context_read_year ctx)) $
783 void $ R.many $ R.spaceHorizontal
784 void $ R.char read_transaction_date_sep
785 R.many $ R.spaceHorizontal
786 let transaction_dates = (date_, dates_)
788 transaction_status <- read_status
789 transaction_code <- R.option "" $ R.try read_code
791 transaction_wording <- read_wording
793 transaction_comments_after <- read_comments
794 let transaction_tags =
797 (tags_of_comments transaction_comments_before)
798 (tags_of_comments transaction_comments_after)
800 (postings_unchecked, postings_not_regular) <-
801 first (postings_by_account . List.map
802 (\(Posting_Typed _ p) -> p)) .
803 List.partition (\(Posting_Typed pt _) ->
804 Posting_Type_Regular == pt) <$>
805 R.many1_separated read_posting R.newline
806 let (transaction_virtual_postings, balanced_virtual_postings_unchecked) =
807 join (***) (postings_by_account . List.map
808 (\(Posting_Typed _ p) -> p)) $
809 List.partition (\(Posting_Typed pt _) ->
810 Posting_Type_Virtual == pt)
815 , transaction_comments_before
816 , transaction_comments_after
818 , transaction_wording
819 , transaction_postings=postings_unchecked
820 , transaction_sourcepos
824 let styles = journal_amount_styles $ context_read_journal ctx
825 transaction_postings <-
826 case H.balance_infer_equilibrium postings_unchecked of
827 (_, Left ko) -> R.parserFailWith "transaction: balance_infer_equilibrium" $
828 Error_Read_transaction_not_equilibrated styles tr_unchecked ko
829 (_bal, Right ok) -> return ok
830 transaction_balanced_virtual_postings <-
831 case H.balance_infer_equilibrium balanced_virtual_postings_unchecked of
832 (_, Left ko) -> R.parserFailWith "transaction: balance_infer_equilibrium" $
833 Error_Read_virtual_transaction_not_equilibrated styles tr_unchecked ko
834 (_bal, Right ok) -> return ok
837 { transaction_postings =
838 Map.unionsWith mappend
839 [ transaction_postings
840 , (set_virtual_tag <$>) <$> transaction_virtual_postings
841 , (set_virtual_tag <$>) <$> transaction_balanced_virtual_postings
845 set_virtual_tag :: Posting -> Posting
847 p@Posting{posting_tags=H.Posting_Tags (H.Tags attrs)} =
848 p{posting_tags = H.Posting_Tags $ H.Tags $ Map.insert ("Virtual":|[]) [] attrs}
850 read_transaction_date_sep :: Char
851 read_transaction_date_sep = '='
856 => ParsecT s (Context_Read c j) m Code
857 read_code = (<?> "code") $
858 (fromString <$>) $ do
860 R.between (R.char '(') (R.char ')') $
861 R.many $ R.satisfy (\c -> c /= ')' && not (R.isSpaceHorizontal c))
865 => ParsecT s u m Wording
866 read_wording = (<?> "wording") $
868 R.many $ R.try read_wording_char
870 read_wording_char :: Stream s m Char => ParsecT s u m Char
871 read_wording_char = do
874 _ | c == read_comment_prefix -> R.parserZero
875 _ | R.isSpaceHorizontal c -> return c <* R.lookAhead (R.try $ read_wording_char)
876 _ | not (Char.isSpace c) -> return c
882 :: (Consable c j, Stream s m Char)
883 => ParsecT s (Context_Read c j) m ()
884 read_directive_alias = do
885 _ <- R.string "alias"
886 R.skipMany1 $ R.spaceHorizontal
887 pat <- read_account_pattern
894 Account_Pattern_Exact acct ->
895 R.modifyState $ \ctx -> ctx{context_read_aliases_exact=
896 Map.insert acct repl $ context_read_aliases_exact ctx}
897 Account_Pattern_Joker jokr ->
898 R.modifyState $ \ctx -> ctx{context_read_aliases_joker=
899 (jokr, repl):context_read_aliases_joker ctx}
900 Account_Pattern_Regex regx ->
901 R.modifyState $ \ctx -> ctx{context_read_aliases_regex=
902 (regx, repl):context_read_aliases_regex ctx}
906 :: (Consable c j, Stream s m Char)
907 => ParsecT s (Context_Read c j) m ()
908 read_default_year = (<?> "default_year") $ do
909 year <- R.integer_of_digits 10 <$> R.many1 R.digit
911 context_read_ <- R.getState
912 R.setState context_read_{context_read_year=year}
914 read_default_unit_and_style
917 => ParsecT s (Context_Read c j) m ()
918 read_default_unit_and_style = (<?> "default_unit_and_style") $ do
919 (sty, amt) <- read_amount
922 let unit = H.amount_unit amt
924 { context_read_journal =
925 let jnl = context_read_journal ctx in
927 { journal_amount_styles =
928 let Amount_Styles styles =
929 journal_amount_styles jnl in
931 Map.insertWith const unit sty styles
933 , context_read_unit = Just unit
939 , Stream s (R.State_Error Error_Read IO) Char
940 ) => ParsecT s (Context_Read c j) (R.State_Error Error_Read IO) ()
941 read_include = (<?> "include") $ do
942 sourcepos <- R.getPosition
943 filename <- R.manyTill R.anyChar (R.lookAhead (R.try R.newline <|> R.eof))
944 context_read_including <- R.getState
945 let journal_including = context_read_journal context_read_including
946 let cwd = FilePath.takeDirectory (R.sourceName sourcepos)
947 journal_file <- liftIO $ FilePath.path_absolute cwd filename
949 join $ liftIO $ Exn.catch
950 (return <$> Text.IO.readFile journal_file)
951 (return . R.parserFailWith "include reading" . Error_Read_reading_file journal_file)
952 (journal_included, context_read_included) <- do
954 R.runParserTWithError
955 (R.and_state $ read_journal_rec journal_file)
956 context_read_including
957 { context_read_journal =
959 { journal_chart = journal_chart journal_including
960 , journal_amount_styles = journal_amount_styles journal_including
965 Right ok -> return ok
966 Left ko -> R.parserFailWith "include parsing" $
967 Error_Read_including_file journal_file ko
969 context_read_included
970 { context_read_journal =
973 journal_included{ journal_files = [journal_file] } :
974 journal_includes journal_including
976 journal_chart journal_included
977 , journal_amount_styles =
978 journal_amount_styles journal_included
986 , Stream s (R.State_Error Error_Read IO) Char
987 ) => ParsecT s (Context_Read c j) (R.State_Error Error_Read IO) ()
988 read_chart = (<?> "chart") $ do
989 -- sourcepos <- R.getPosition
994 tags_ <- R.many_separated
995 (R.try (R.skipMany1 R.spaceHorizontal >> read_tag
996 <* read_hspaces <* read_comments))
1000 TreeMap.singleton acct $
1002 H.tag_from_List tags_
1006 (flip (\(p:|ps, v) ->
1007 TreeMap.insert mappend
1008 (p:|ps `mappend` [v])
1014 let j = context_read_journal ctx
1016 ctx{context_read_journal=
1032 , Stream s (R.State_Error Error_Read IO) Char
1034 -> ParsecT s (Context_Read c j)
1035 (R.State_Error Error_Read IO)
1037 read_journal filepath = (<?> "journal") $ do
1038 currentLocalTime <- liftIO $
1040 <$> Time.getCurrentTimeZone
1041 <*> Time.getCurrentTime
1042 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
1044 R.setState $ ctx{context_read_year=currentLocalYear}
1045 read_journal_rec filepath
1050 , Stream s (R.State_Error Error_Read IO) Char
1053 -> ParsecT s (Context_Read c j)
1054 (R.State_Error Error_Read IO)
1056 read_journal_rec journal_file = do
1057 last_read_time <- liftIO H.date_now
1065 journal_ <- context_read_journal <$> R.getState
1068 { journal_files = [journal_file]
1069 , journal_includes = List.reverse $ journal_includes journal_
1070 , journal_last_read_time = last_read_time
1075 => ParsecT s u m (ParsecT s u m ())
1078 R.skipMany (read_hspaces >> R.newline)
1080 R.skipMany (read_hspaces >> R.newline)
1081 R.try (read_hspaces >> R.eof) <|> loop r
1085 , u ~ Context_Read c j
1086 , m ~ R.State_Error Error_Read IO
1088 => ParsecT s u m (ParsecT s u m ())
1091 _ <- R.lookAhead (R.try $ R.char read_comment_prefix)
1093 _cmts <- read_comments
1095 R.modifyState $ \ctx ->
1096 let j = context_read_journal ctx in
1097 ctx{context_read_journal=
1099 mcons (context_read_filter ctx) cmts $
1107 , u ~ Context_Read c j
1108 , m ~ R.State_Error Error_Read IO
1110 => ParsecT s u m (ParsecT s u m ())
1112 let choice s = R.string s >> R.skipMany1 R.spaceHorizontal
1115 [ choice "Y" >> return read_default_year
1116 , choice "D" >> return read_default_unit_and_style
1117 , choice "!include" >> return read_include
1122 , u ~ Context_Read c j
1123 , m ~ R.State_Error Error_Read IO
1125 => ParsecT s u m (ParsecT s u m ())
1126 jump_transaction = do
1127 _ <- R.lookAhead $ R.try (R.many1 R.digit >> R.char read_date_ymd_sep)
1129 t <- read_transaction
1130 R.modifyState $ \ctx ->
1131 let j = context_read_journal ctx in
1132 ctx{context_read_journal=
1135 (context_read_cons ctx $
1136 Charted (journal_chart j) t)
1137 (journal_content j)}}
1141 , u ~ Context_Read c j
1142 , m ~ R.State_Error Error_Read IO
1144 => ParsecT s u m (ParsecT s u m ())
1151 :: (Consable c j, Monoid j)
1154 -> ExceptT [R.Error Error_Read] IO (Journal j)
1155 read_file ctx path =
1158 (Right <$> Text.IO.readFile path) $
1159 \ko -> return $ Left $
1160 [R.Error_Custom (R.initialPos path) $
1161 Error_Read_reading_file path ko])
1162 >>= liftIO . R.runParserTWithError
1163 (read_journal path) ctx path
1165 Left ko -> throwE $ ko
1166 Right ok -> ExceptT $ return $ Right ok