1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE FlexibleContexts #-}
3 {-# LANGUAGE NamedFieldPuns #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE ScopedTypeVariables #-}
6 {-# LANGUAGE TupleSections #-}
7 module Hcompta.Format.Ledger.Read where
9 import Control.Applicative ((<$>), (<*>), (<*))
10 import qualified Control.Exception as Exception
11 import Control.Arrow ((***))
12 import Control.Monad (guard, join, liftM)
13 import Control.Monad.IO.Class (liftIO)
14 import Control.Monad.Trans.Except (ExceptT(..), throwE)
15 import Control.Monad.Trans.Class (lift)
16 import qualified Data.Char
17 import qualified Data.Decimal
18 import qualified Data.Either
19 import qualified Data.List
20 import Data.List.NonEmpty (NonEmpty(..))
21 import qualified Data.Map.Strict as Data.Map
22 import Data.Maybe (fromMaybe)
23 import Data.String (fromString)
24 import qualified Data.Time.Calendar as Time
25 import qualified Data.Time.Clock as Time
26 import qualified Data.Time.LocalTime as Time
27 import Data.Time.LocalTime (TimeZone(..))
28 import Data.Typeable ()
29 import qualified Text.Parsec as R hiding
41 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
42 import qualified Text.Parsec.Pos as R
43 import qualified Data.Text.IO as Text.IO (readFile)
44 import qualified Data.Text as Text
45 import qualified System.FilePath.Posix as Path
47 import qualified Hcompta.Calc.Balance as Calc.Balance
48 import qualified Hcompta.Model.Account as Account
49 import Hcompta.Model.Account (Account)
50 import qualified Hcompta.Model.Amount as Amount
51 import Hcompta.Model.Amount (Amount)
52 import qualified Hcompta.Model.Amount.Style as Style
53 import qualified Hcompta.Model.Amount.Unit as Unit
54 import Hcompta.Model.Amount.Unit (Unit)
55 import qualified Hcompta.Model.Date as Date
56 import Hcompta.Model.Date (Date)
57 import qualified Hcompta.Format.Ledger as Ledger
58 import Hcompta.Format.Ledger
61 , Posting(..), Posting_Type(..)
62 , Tag, Tag_Name, Tag_Value, Tag_by_Name
65 import qualified Hcompta.Lib.Regex as Regex
66 import Hcompta.Lib.Regex (Regex)
67 import qualified Hcompta.Lib.Parsec as R
68 import qualified Hcompta.Lib.Path as Path
72 { context_account_prefix :: !(Maybe Account)
73 , context_aliases_exact :: !(Data.Map.Map Account Account)
74 , context_aliases_joker :: ![(Account.Joker, Account)]
75 , context_aliases_regex :: ![(Regex, Account)]
76 , context_date :: !Date
77 , context_unit_and_style :: !(Maybe (Amount.Unit, Amount.Style))
78 , context_journal :: !Journal
79 , context_year :: !Date.Year
82 nil_Context :: Context
85 { context_account_prefix = Nothing
86 , context_aliases_exact = Data.Map.empty
87 , context_aliases_joker = []
88 , context_aliases_regex = []
89 , context_date = Date.nil
90 , context_unit_and_style = Nothing
91 , context_journal = Ledger.journal
92 , context_year = (\(year, _ , _) -> year) $
93 Time.toGregorian $ Time.utctDay $
94 journal_last_read_time Ledger.journal
98 = Error_year_or_day_is_missing
99 | Error_invalid_day (Integer, Int, Int)
100 | Error_invalid_time_of_day (Int, Int, Integer)
101 | Error_transaction_not_equilibrated Transaction [Calc.Balance.Unit_Sum Amount]
102 | Error_virtual_transaction_not_equilibrated Transaction [Calc.Balance.Unit_Sum Amount]
103 | Error_reading_file FilePath Exception.IOException
104 | Error_including_file FilePath [R.Error Error]
107 -- | Parse either '-' into 'negate', or '+' or '' into 'id'.
108 sign :: (Stream s m Char, Num i) => ParsecT s u m (i -> i)
110 (R.char '-' >> return negate)
111 <|> (R.char '+' >> return id)
114 -- * Parsing 'Account'
116 account_name_sep :: Char
117 account_name_sep = ':'
119 -- | Parse an 'Account'.
120 account :: Stream s m Char => ParsecT s u m Account
122 R.notFollowedBy $ R.space_horizontal
123 Account.from_List <$> do
124 R.many1_separated account_name $ R.char account_name_sep
126 -- | Parse an Account.'Account.Name'.
127 account_name :: Stream s m Char => ParsecT s u m Account.Name
130 R.many1 $ R.try account_name_char
132 account_name_char :: Stream s m Char => ParsecT s u m Char
133 account_name_char = do
136 _ | c == comment_begin -> R.parserZero
137 _ | c == account_name_sep -> R.parserZero
138 _ | R.is_space_horizontal c -> do
139 _ <- R.notFollowedBy $ R.space_horizontal
140 return c <* (R.lookAhead $ R.try $
141 ( R.try (R.char account_name_sep)
142 <|> account_name_char
144 _ | not (Data.Char.isSpace c) -> return c
147 -- | Parse an Account.'Account.Joker_Name'.
148 account_joker_name :: Stream s m Char => ParsecT s u m Account.Joker_Name
149 account_joker_name = do
150 n <- R.option Nothing $ (Just <$> account_name)
152 Nothing -> R.char account_name_sep >> (return $ Account.Joker_Any)
153 Just n' -> return $ Account.Joker_Name n'
155 -- | Parse an Account.'Account.Joker'.
156 account_joker :: Stream s m Char => ParsecT s u m Account.Joker
158 R.notFollowedBy $ R.space_horizontal
159 R.many1_separated account_joker_name $ R.char account_name_sep
161 -- | Parse a 'Regex'.
162 account_regex :: Stream s m Char => ParsecT s u m Regex
164 re <- R.many1 $ R.satisfy (not . R.is_space_horizontal)
167 -- | Parse an Account.'Account.Filter'.
168 account_pattern :: Stream s m Char => ParsecT s u m Account.Pattern
171 [ Account.Pattern_Exact <$> (R.char '=' >> account)
172 , Account.Pattern_Joker <$> (R.char '*' >> account_joker)
173 , Account.Pattern_Regex <$> (R.option '~' (R.char '~') >> account_regex)
176 -- * Parsing 'Amount'
178 -- | Parse an 'Amount'.
179 amount :: Stream s m Char => ParsecT s u m Amount
183 R.option Nothing $ do
185 s <- R.many $ R.space_horizontal
186 return $ Just $ (u, not $ null s)
187 (quantity_, style) <- do
194 , grouping_fractional
197 [ quantity '_' ',' '_' <* (R.notFollowedBy $ R.oneOf ",._")
198 , quantity '_' '.' '_' <* (R.notFollowedBy $ R.oneOf ",._")
199 , quantity ',' '.' '_' <* (R.notFollowedBy $ R.oneOf ",._")
200 , quantity '.' ',' '_' <* (R.notFollowedBy $ R.oneOf ",._")
202 let int = Data.List.concat integral
203 let frac_flat = Data.List.concat fractional
204 let frac = reverse $ Data.List.dropWhile (== '0') $ reverse $ frac_flat
205 let place = length frac
207 let mantissa = R.integer_of_digits 10 $ int ++ frac
209 ( Data.Decimal.Decimal
214 , Style.grouping_integral
215 , Style.grouping_fractional
216 , Style.precision = fromIntegral $ length frac_flat
219 (unit_, unit_side, unit_spaced) <-
222 return (u, Just Style.Side_Left, Just s)
224 R.option (Unit.nil, Nothing, Nothing) $ R.try $ do
225 s <- R.many $ R.space_horizontal
227 return $ (u, Just Style.Side_Right, Just $ not $ null s)
230 { Amount.quantity = left_signing $ quantity_
231 , Amount.style = style
235 , Amount.unit = unit_
240 { integral :: [String]
241 , fractional :: [String]
242 , fractioning :: Maybe Style.Fractioning
243 , grouping_integral :: Maybe Style.Grouping
244 , grouping_fractional :: Maybe Style.Grouping
247 -- | Parse a 'Quantity'.
250 => Char -- ^ Integral grouping separator.
251 -> Char -- ^ Fractioning separator.
252 -> Char -- ^ Fractional grouping separator.
253 -> ParsecT s u m Quantity
254 quantity int_group_sep frac_sep frac_group_sep = do
255 (integral, grouping_integral) <- do
258 [] -> return ([], Nothing)
260 t <- R.many $ R.char int_group_sep >> R.many1 R.digit
262 return (digits, grouping_of_digits int_group_sep digits)
263 (fractional, fractioning, grouping_fractional) <-
266 _ -> R.option ([], Nothing, Nothing)) $ do
267 fractioning <- R.char frac_sep
269 t <- R.many $ R.char frac_group_sep >> R.many1 R.digit
271 return (digits, Just fractioning
272 , grouping_of_digits frac_group_sep $ reverse digits)
279 , grouping_fractional
282 grouping_of_digits :: Char -> [String] -> Maybe Style.Grouping
283 grouping_of_digits group_sep digits =
288 Style.Grouping group_sep $
289 canonicalize_grouping $
291 canonicalize_grouping :: [Int] -> [Int]
292 canonicalize_grouping groups =
293 Data.List.foldl -- NOTE: remove duplicates at beginning and reverse.
294 (\acc l0 -> case acc of
295 l1:_ -> if l0 == l1 then acc else l0:acc
297 case groups of -- NOTE: keep only longer at beginning.
298 l0:l1:t -> if l0 > l1 then groups else l1:t
301 -- | Parse an 'Unit'.
302 unit :: Stream s m Char => ParsecT s u m Unit
304 (quoted <|> unquoted) <?> "unit"
306 unquoted :: Stream s m Char => ParsecT s u m Unit
311 case Data.Char.generalCategory c of
312 Data.Char.CurrencySymbol -> True
313 Data.Char.LowercaseLetter -> True
314 Data.Char.ModifierLetter -> True
315 Data.Char.OtherLetter -> True
316 Data.Char.TitlecaseLetter -> True
317 Data.Char.UppercaseLetter -> True
319 quoted :: Stream s m Char => ParsecT s u m Unit
322 R.between (R.char '"') (R.char '"') $
328 directive_alias :: Stream s m Char => ParsecT s Context m ()
330 _ <- R.string "alias"
331 R.skipMany1 $ R.space_horizontal
332 pattern <- account_pattern
333 R.skipMany $ R.space_horizontal
335 R.skipMany $ R.space_horizontal
337 R.skipMany $ R.space_horizontal
339 Account.Pattern_Exact acct -> R.modifyState $ \ctx -> ctx{context_aliases_exact=
340 Data.Map.insert acct repl $ context_aliases_exact ctx}
341 Account.Pattern_Joker jokr -> R.modifyState $ \ctx -> ctx{context_aliases_joker=
342 (jokr, repl):context_aliases_joker ctx}
343 Account.Pattern_Regex regx -> R.modifyState $ \ctx -> ctx{context_aliases_regex=
344 (regx, repl):context_aliases_regex ctx}
347 -- | Parse the year, month and day separator: '/' or '-'.
348 date_separator :: Stream s m Char => ParsecT s u m Char
349 date_separator = R.satisfy (\c -> c == '/' || c == '-')
351 -- | Parse the hour, minute and second separator: ':'.
352 hour_separator :: Stream s m Char => ParsecT s u m Char
353 hour_separator = R.char ':'
357 -- | Parse a 'Date' in [YYYY/]MM/DD [HH:MM[:SS][TZ]] format.
359 :: (Stream s (R.Error_State Error m) Char, Monad m)
360 => Maybe Integer -> ParsecT s u (R.Error_State Error m) Date
362 n0 <- R.many1 R.digit
363 day_sep <- date_separator
364 n1 <- R.try (R.count 2 R.digit) <|> R.count 1 R.digit
365 n2 <- R.option Nothing $ R.try $ do
367 Just <$> do R.try (R.count 2 R.digit) <|> R.count 1 R.digit
369 case (n2, def_year) of
370 (Nothing, Nothing) -> R.fail_with "date" (Error_year_or_day_is_missing)
371 (Nothing, Just year) -> return (year, n0, n1)
372 (Just d, _) -> return (R.integer_of_digits 10 n0, n1, d)
373 let month = fromInteger $ R.integer_of_digits 10 m
374 let day = fromInteger $ R.integer_of_digits 10 d
375 guard $ month >= 1 && month <= 12
376 guard $ day >= 1 && day <= 31
377 day_ <- case Time.fromGregorianValid year month day of
378 Nothing -> R.fail_with "date" (Error_invalid_day (year, month, day))
379 Just day_ -> return day_
380 (hour, minu, sec, tz) <-
381 R.option (0, 0, 0, Time.utc) $ R.try $ do
382 R.skipMany1 $ R.space_horizontal
383 hour <- R.try (R.count 2 R.digit) <|> R.count 1 R.digit
384 sep <- hour_separator
385 minu <- R.try (R.count 2 R.digit) <|> R.count 1 R.digit
386 sec <- R.option Nothing $ R.try $ do
388 Just <$> (R.try (R.count 2 R.digit) <|> R.count 1 R.digit)
389 tz <- R.option Time.utc $ R.try $ do
390 R.skipMany $ R.space_horizontal
393 ( fromInteger $ R.integer_of_digits 10 hour
394 , fromInteger $ R.integer_of_digits 10 minu
395 , maybe 0 (R.integer_of_digits 10) sec
397 tod <- case Time.makeTimeOfDayValid hour minu (fromInteger sec) of
398 Nothing -> R.fail_with "date" (Error_invalid_time_of_day (hour, minu, sec))
399 Just tod -> return tod
402 (Time.LocalTime day_ tod)
406 time_zone :: Stream s m Char => ParsecT s u m TimeZone
408 -- DOC: http://www.timeanddate.com/time/zones/
409 -- TODO: only a few time zones are suported below.
410 -- TODO: check the timeZoneSummerOnly values
412 [ R.char 'A' >> R.choice
413 [ R.string "ST" >> return (TimeZone ((-4) * 60) False "AST")
414 , R.string "DT" >> return (TimeZone ((-3) * 60) True "ADT")
415 , return (TimeZone ((-1) * 60) False "A")
417 , R.char 'B' >> R.choice
418 [ R.string "ST" >> return (TimeZone ((-11) * 60) False "BST")
419 , R.string "DT" >> return (TimeZone ((-10) * 60) True "BDT")
421 , R.char 'C' >> R.choice
422 [ R.char 'E' >> R.choice
423 [ R.string "T" >> return (TimeZone ((1) * 60) True "CET")
424 , R.string "ST" >> return (TimeZone ((2) * 60) False "CEST")
426 , R.string "ST" >> return (TimeZone ((-6) * 60) False "CST")
427 , R.string "DT" >> return (TimeZone ((-5) * 60) True "CDT")
429 , R.char 'E' >> R.choice
430 [ R.string "ST" >> return (TimeZone ((-5) * 60) False "EST")
431 , R.string "DT" >> return (TimeZone ((-4) * 60) True "EDT")
433 , R.string "GMT" >> return (TimeZone 0 False "GMT")
434 , R.char 'H' >> R.choice
435 [ R.string "ST" >> return (TimeZone ((-10) * 60) False "HST")
436 , R.string "DT" >> return (TimeZone (( -9) * 60) True "HDT")
438 , R.char 'M' >> R.choice
439 [ R.string "ST" >> return (TimeZone ((-7) * 60) False "MST")
440 , R.string "DT" >> return (TimeZone ((-6) * 60) True "MDT")
441 , return (TimeZone ((-12) * 60) False "M")
443 , R.char 'N' >> R.choice
444 [ R.string "ST" >> return (TimeZone ((-3) * 60 - 30) False "NST")
445 , return (TimeZone (1 * 60) False "N")
447 , R.char 'P' >> R.choice
448 [ R.string "ST" >> return (TimeZone ((-8) * 60) False "PST")
449 , R.string "DT" >> return (TimeZone ((-7) * 60) True "PDT")
451 , R.char 'Y' >> R.choice
452 [ R.string "ST" >> return (TimeZone ((-9) * 60) False "YST")
453 , R.string "DT" >> return (TimeZone ((-8) * 60) True "YDT")
454 , return (TimeZone (12 * 60) False "Y")
456 , R.char 'Z' >> return (TimeZone 0 False "Z")
460 time_zone_digits :: Stream s m Char => ParsecT s u m TimeZone
461 {-# INLINEABLE time_zone_digits #-}
462 time_zone_digits = do
464 hour <- R.integer_of_digits 10 <$> R.count 2 R.digit
465 _ <- R.option ':' (R.char ':')
466 minute <- R.integer_of_digits 10 <$> R.count 2 R.digit
468 { timeZoneMinutes = sign_ (fromInteger hour * 60 + fromInteger minute)
469 , timeZoneSummerOnly = False
470 , timeZoneName = Time.timeZoneOffsetString tz
474 -- * Parsing 'Comment'
476 comment_begin :: Char
479 comment :: Stream s m Char => ParsecT s u m Comment
481 _ <- R.char comment_begin
483 R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
486 comments :: Stream s m Char => ParsecT s u m [Comment]
490 R.many1_separated comment (R.new_line >> R.skipMany R.space_horizontal)
496 tag_value_sep :: Char
503 tag :: Stream s m Char => ParsecT s u m Tag
506 _ <- R.char tag_value_sep
511 tag_name :: Stream s m Char => ParsecT s u m Tag_Name
514 R.many1 $ R.satisfy (\c -> c /= tag_value_sep && not (Data.Char.isSpace c))
516 tag_value :: Stream s m Char => ParsecT s u m Tag_Value
519 R.manyTill R.anyChar $ do
521 R.try (R.char tag_sep >> R.many R.space_horizontal >> tag_name >> R.char tag_value_sep >> return ())
525 tags :: Stream s m Char => ParsecT s u m Tag_by_Name
527 Ledger.tag_by_Name <$> do
528 R.many_separated tag $ do
530 R.skipMany $ R.space_horizontal
533 not_tag :: Stream s m Char => ParsecT s u m ()
535 R.skipMany $ R.try $ do
536 R.skipMany $ R.satisfy
537 (\c -> c /= tag_value_sep
538 && not (Data.Char.isSpace c))
541 -- * Parsing 'Posting'
543 -- | Parse a 'Posting'.
545 :: (Stream s (R.Error_State Error m) Char, Monad m)
546 => ParsecT s Context (R.Error_State Error m) (Posting, Posting_Type)
549 sourcepos <- R.getPosition
550 R.skipMany1 $ R.space_horizontal
552 R.skipMany $ R.space_horizontal
554 let (type_, account_) = posting_type acct
558 _ <- R.count 2 R.space_horizontal
559 R.skipMany $ R.space_horizontal
561 if u == Unit.nil then id
563 Data.Map.adjust (\a ->
564 a{ Amount.style = s{Style.precision = Style.precision $ Amount.style a}
567 (context_unit_and_style ctx) .
568 Amount.from_List <$> do
569 R.many_separated amount $ do
570 R.skipMany $ R.space_horizontal
571 _ <- R.char amount_sep
572 R.skipMany $ R.space_horizontal
574 , return Data.Map.empty
576 R.skipMany $ R.space_horizontal
577 -- TODO: balance assertion
579 comments_ <- comments
580 let tags_ = tags_of_comments comments_
582 case Data.Map.lookup "date" tags_ of
585 let date2s = Data.Map.lookup "date2" tags_ -- NOTE: support hledger's date2
587 (flip mapM) (dates ++ fromMaybe [] date2s) $ \s ->
588 R.runParserT_with_Error_fail "tag date"
589 (date (Just $ context_year ctx) <* R.eof) ()
591 >>= \dates_ -> case (dates, date2s) of -- NOTE: put hledger's date2 at least in second position
593 return $ context_date ctx:dates_
596 { posting_account=account_
597 , posting_amounts=amounts_
598 , posting_comments=comments_
599 , posting_dates=dates_
600 , posting_sourcepos=sourcepos
601 , posting_status=status_
609 tags_of_comments :: [Comment] -> Tag_by_Name
611 Data.Map.unionsWith (flip (++))
613 ( Data.Either.either (const Data.Map.empty) id
614 . R.runParser (not_tag >> tags <* R.eof) () "" )
616 status :: Stream s m Char => ParsecT s u m Ledger.Status
619 R.skipMany $ R.space_horizontal
620 _ <- (R.char '*' <|> R.char '!')
625 -- | Return the 'Posting_Type' and stripped 'Account' of the given 'Account'.
626 posting_type :: Account -> (Posting_Type, Account)
628 fromMaybe (Posting_Type_Regular, acct) $ do
631 case Text.stripPrefix virtual_begin name of
634 Text.stripSuffix virtual_end name'
635 >>= return . Text.strip
636 guard $ not $ Text.null name''
637 Just (Posting_Type_Virtual, name'':|[])
640 Text.stripPrefix virtual_balanced_begin name
641 >>= Text.stripSuffix virtual_balanced_end
642 >>= return . Text.strip
643 guard $ not $ Text.null name'
644 Just (Posting_Type_Virtual_Balanced, name':|[])
645 first_name:|acct' -> do
646 let rev_acct' = Data.List.reverse acct'
647 let last_name = Data.List.head rev_acct'
648 case Text.stripPrefix virtual_begin first_name
649 >>= return . Text.stripStart of
650 Just first_name' -> do
652 Text.stripSuffix virtual_end last_name
653 >>= return . Text.stripEnd
654 guard $ not $ Text.null first_name'
655 guard $ not $ Text.null last_name'
657 ( Posting_Type_Virtual
659 Data.List.reverse (last_name':Data.List.tail rev_acct')
663 Text.stripPrefix virtual_balanced_begin first_name
664 >>= return . Text.stripStart
666 Text.stripSuffix virtual_balanced_end last_name
667 >>= return . Text.stripEnd
668 guard $ not $ Text.null first_name'
669 guard $ not $ Text.null last_name'
671 ( Posting_Type_Virtual_Balanced
673 Data.List.reverse (last_name':Data.List.tail rev_acct')
676 virtual_begin = Text.singleton posting_type_virtual_begin
677 virtual_end = Text.singleton posting_type_virtual_end
678 virtual_balanced_begin = Text.singleton posting_type_virtual_balanced_begin
679 virtual_balanced_end = Text.singleton posting_type_virtual_balanced_end
681 posting_type_virtual_begin :: Char
682 posting_type_virtual_begin = '('
683 posting_type_virtual_balanced_begin :: Char
684 posting_type_virtual_balanced_begin = '['
685 posting_type_virtual_end :: Char
686 posting_type_virtual_end = ')'
687 posting_type_virtual_balanced_end :: Char
688 posting_type_virtual_balanced_end = ']'
690 -- * Parsing 'Transaction'
693 :: (Stream s (R.Error_State Error m) Char, Monad m)
694 => ParsecT s Context (R.Error_State Error m) Transaction
696 sourcepos <- R.getPosition
702 _ -> return x <* R.new_line
703 date_ <- date (Just $ context_year ctx)
705 R.option [] $ R.try $ do
706 R.skipMany $ R.space_horizontal
708 R.skipMany $ R.space_horizontal
710 (date (Just $ context_year ctx)) $
712 R.many $ R.space_horizontal
714 >> (R.many $ R.space_horizontal)
715 R.skipMany $ R.space_horizontal
717 code_ <- R.option "" $ R.try code
718 R.skipMany $ R.space_horizontal
719 description_ <- description
720 R.skipMany $ R.space_horizontal
721 comments_after <- comments
723 Data.Map.unionWith (++)
724 (tags_of_comments comments_before)
725 (tags_of_comments comments_after)
727 (postings_unchecked, postings_not_regular) <-
728 ((Ledger.posting_by_Account . Data.List.map fst) *** id) .
729 Data.List.partition ((Posting_Type_Regular ==) . snd) <$>
730 R.many1_separated posting R.new_line
731 let (virtual_postings, balanced_virtual_postings_unchecked) =
732 join (***) (Ledger.posting_by_Account . Data.List.map fst) $
733 Data.List.partition ((Posting_Type_Virtual ==) . snd)
737 { transaction_code=code_
738 , transaction_comments_before=comments_before
739 , transaction_comments_after=comments_after
740 , transaction_dates=(date_, dates_)
741 , transaction_description=description_
742 , transaction_postings=postings_unchecked
743 , transaction_virtual_postings=virtual_postings
744 , transaction_balanced_virtual_postings=balanced_virtual_postings_unchecked
745 , transaction_sourcepos=sourcepos
746 , transaction_status=status_
747 , transaction_tags=tags_
750 case snd $ Calc.Balance.infer_equilibrium postings_unchecked of
751 Left ko -> R.fail_with "transaction infer_equilibrium"
752 (Error_transaction_not_equilibrated tr_unchecked ko)
753 Right ok -> return ok
754 balanced_virtual_postings <-
755 case snd $ Calc.Balance.infer_equilibrium balanced_virtual_postings_unchecked of
756 Left ko -> R.fail_with "transaction infer_equilibrium"
757 (Error_virtual_transaction_not_equilibrated tr_unchecked ko)
758 Right ok -> return ok
761 { transaction_code=code_
762 , transaction_comments_before=comments_before
763 , transaction_comments_after=comments_after
764 , transaction_dates=(date_, dates_)
765 , transaction_description=description_
766 , transaction_postings=postings
767 , transaction_virtual_postings=virtual_postings
768 , transaction_balanced_virtual_postings=balanced_virtual_postings
769 , transaction_sourcepos=sourcepos
770 , transaction_status=status_
771 , transaction_tags=tags_
778 code :: Stream s m Char => ParsecT s Context m Ledger.Code
781 R.skipMany $ R.space_horizontal
782 R.between (R.char '(') (R.char ')') $
783 R.many $ R.satisfy (\c -> c /= ')' && not (R.is_space_horizontal c))
786 description :: Stream s m Char => ParsecT s u m Ledger.Description
789 R.many $ R.try description_char
792 description_char :: Stream s m Char => ParsecT s u m Char
793 description_char = do
796 _ | c == comment_begin -> R.parserZero
797 _ | R.is_space_horizontal c -> return c <* (R.lookAhead $ R.try $ description_char)
798 _ | not (Data.Char.isSpace c) -> return c
801 -- * Parsing directives
803 default_year :: Stream s m Char => ParsecT s Context m ()
805 year <- R.integer_of_digits 10 <$> R.many1 R.digit
806 R.skipMany R.space_horizontal >> R.new_line
807 context_ <- R.getState
808 R.setState context_{context_year=year}
811 default_unit_and_style :: Stream s m Char => ParsecT s Context m ()
812 default_unit_and_style = (do
814 R.skipMany R.space_horizontal >> R.new_line
815 context_ <- R.getState
816 R.setState context_{context_unit_and_style =
818 ( Amount.unit amount_
819 , Amount.style amount_ )}
820 ) <?> "default unit and style"
823 :: Stream s (R.Error_State Error IO) Char
824 => ParsecT s Context (R.Error_State Error IO) ()
826 sourcepos <- R.getPosition
827 filename <- R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
828 context_ <- R.getState
829 let journal_ = context_journal context_
830 let cwd = Path.takeDirectory (R.sourceName sourcepos)
831 file_path <- liftIO $ Path.abs cwd filename
833 liftIO $ Exception.catch
834 (liftM return $ readFile file_path)
835 (return . R.fail_with "include reading" . Error_reading_file file_path)
837 (journal_included, context_included) <- do
839 R.runParserT_with_Error (R.and_state $ journal_rec file_path)
840 context_{context_journal = Ledger.journal}
843 Right ok -> return ok
844 Left ko -> R.fail_with "include parsing" (Error_including_file file_path ko)
846 context_included{context_journal=
847 journal_{journal_includes=
848 journal_included{journal_file=file_path}
849 : journal_includes journal_}}
852 -- * Parsing 'Journal'
855 :: Stream s (R.Error_State Error IO) Char
857 -> ParsecT s Context (R.Error_State Error IO) Journal
859 currentLocalTime <- liftIO $
861 <$> Time.getCurrentTimeZone
862 <*> Time.getCurrentTime
863 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
864 context_ <- R.getState
865 R.setState $ context_{context_year=currentLocalYear}
870 :: Stream s (R.Error_State Error IO) Char
872 -> ParsecT s Context (R.Error_State Error IO) Journal
873 journal_rec file_ = do
874 last_read_time <- lift $ liftIO Time.getCurrentTime
877 [ R.skipMany1 R.space
879 [ R.string "Y" >> return default_year
880 , R.string "D" >> return default_unit_and_style
881 , R.string "!include" >> return include
883 >>= \r -> R.skipMany1 R.space_horizontal >> r)
886 context_' <- R.getState
887 let j = context_journal context_'
888 R.setState $ context_'{context_journal=
889 j{journal_transactions=
890 Data.Map.insertWith (flip (++))
891 -- NOTE: flip-ing preserves order but slows down
892 -- when many transactions have the very same date.
893 (Date.to_UTC $ fst $ transaction_dates t) [t]
894 (journal_transactions j)}}
895 R.new_line <|> R.eof))
896 , R.try (comment >> return ())
899 journal_ <- context_journal <$> R.getState
902 { journal_file = file_
903 , journal_last_read_time=last_read_time
904 , journal_includes = reverse $ journal_includes journal_
907 -- ** Parsing 'Journal' from a file
909 file :: FilePath -> ExceptT [R.Error Error] IO Journal
913 (liftM Right $ Text.IO.readFile path) $
914 \ko -> return $ Left $
915 [ R.Error_Custom (R.initialPos path) $ Error_reading_file path ko ]
916 >>= liftIO . R.runParserT_with_Error (journal path) nil_Context path
918 Left ko -> throwE $ ko
919 Right ok -> ExceptT $ return $ Right ok