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 qualified Data.Time.Calendar as Time
24 import qualified Data.Time.Clock as Time
25 import qualified Data.Time.LocalTime as Time
26 import Data.Time.LocalTime (TimeZone(..))
27 import Data.Typeable ()
28 import qualified Text.Parsec as R
29 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
30 import qualified Text.Parsec.Error as R
31 import qualified Text.Parsec.Pos as R
32 import qualified Data.Text.IO as Text.IO (readFile)
33 import qualified Data.Text as Text
34 import qualified System.FilePath.Posix as Path
36 import qualified Hcompta.Calc.Balance as Calc.Balance
37 import qualified Hcompta.Model.Account as Account
38 import Hcompta.Model.Account (Account)
39 import qualified Hcompta.Model.Amount as Amount
40 import Hcompta.Model.Amount (Amount)
41 import qualified Hcompta.Model.Amount.Style as Style
42 import qualified Hcompta.Model.Amount.Unit as Unit
43 import Hcompta.Model.Amount.Unit (Unit)
44 import qualified Hcompta.Model.Transaction as Transaction
45 import Hcompta.Model.Transaction (Transaction, Comment)
46 import qualified Hcompta.Model.Transaction.Posting as Posting
47 import Hcompta.Model.Transaction (Posting)
48 import qualified Hcompta.Model.Transaction.Tag as Tag
49 import Hcompta.Model.Transaction (Tag)
50 import qualified Hcompta.Model.Date as Date
51 import Hcompta.Model.Date (Date)
52 import Hcompta.Format.Ledger.Journal as Journal
53 import qualified Hcompta.Lib.Regex as Regex
54 import Hcompta.Lib.Regex (Regex)
55 import qualified Hcompta.Lib.Parsec as R
56 import qualified Hcompta.Lib.Path as Path
60 { context_account_prefix :: !(Maybe Account)
61 , context_aliases_exact :: !(Data.Map.Map Account Account)
62 , context_aliases_joker :: ![(Account.Joker, Account)]
63 , context_aliases_regex :: ![(Regex, Account)]
64 , context_date :: !Date
65 , context_unit_and_style :: !(Maybe (Amount.Unit, Amount.Style))
66 , context_journal :: !Journal
67 , context_year :: !Date.Year
71 = Error_year_or_day_is_missing
72 | Error_invalid_day (Integer, Int, Int)
73 | Error_invalid_time_of_day (Integer, Integer, Integer)
74 | Error_transaction_not_equilibrated [Calc.Balance.Unit_Sum]
75 | Error_virtual_transaction_not_equilibrated [Calc.Balance.Unit_Sum]
76 | Error_reading_file FilePath Exception.IOException
77 | Error_including_file FilePath (R.ParseError, [Error])
80 nil_Context :: Context
83 { context_account_prefix = Nothing
84 , context_aliases_exact = Data.Map.empty
85 , context_aliases_joker = []
86 , context_aliases_regex = []
87 , context_date = Date.nil
88 , context_unit_and_style = Nothing
89 , context_journal = Journal.nil
90 , context_year = (\(year, _ , _) -> year) $
91 Time.toGregorian $ Time.utctDay $
92 Journal.last_read_time Journal.nil
95 -- | Parse either '-' into 'negate', or '+' or '' into 'id'.
96 sign :: (Stream s m Char, Num i) => ParsecT s u m (i -> i)
98 (R.char '-' >> return negate)
99 <|> (R.char '+' >> return id)
102 -- * Parsing 'Account'
104 account_name_sep :: Char
105 account_name_sep = ':'
107 -- | Parse an 'Account'.
108 account :: Stream s m Char => ParsecT s u m Account
110 R.notFollowedBy $ R.space_horizontal
111 Account.from_List <$> do
112 R.many1_separated account_name $ R.char account_name_sep
114 -- | Parse an Account.'Account.Name'.
115 account_name :: Stream s m Char => ParsecT s u m Account.Name
118 R.many1 $ R.try account_name_char
120 account_name_char :: Stream s m Char => ParsecT s u m Char
121 account_name_char = do
124 _ | c == comment_begin -> R.parserZero
125 _ | c == account_name_sep -> R.parserZero
126 _ | R.is_space_horizontal c -> do
127 _ <- R.notFollowedBy $ R.space_horizontal
128 return c <* (R.lookAhead $ R.try $
129 ( R.try (R.char account_name_sep)
130 <|> account_name_char
132 _ | not (Data.Char.isSpace c) -> return c
135 -- | Parse an Account.'Account.Joker_Name'.
136 account_joker_name :: Stream s m Char => ParsecT s u m Account.Joker_Name
137 account_joker_name = do
138 n <- R.option Nothing $ (Just <$> account_name)
140 Nothing -> R.char account_name_sep >> (return $ Account.Joker_Any)
141 Just n' -> return $ Account.Joker_Name n'
143 -- | Parse an Account.'Account.Joker'.
144 account_joker :: Stream s m Char => ParsecT s u m Account.Joker
146 R.notFollowedBy $ R.space_horizontal
147 R.many1_separated account_joker_name $ R.char account_name_sep
149 -- | Parse a 'Regex'.
150 account_regex :: Stream s m Char => ParsecT s u m Regex
152 re <- R.many1 $ R.satisfy (not . R.is_space_horizontal)
155 -- | Parse an Account.'Account.Filter'.
156 account_pattern :: Stream s m Char => ParsecT s u m Account.Pattern
159 [ Account.Pattern_Exact <$> (R.char '=' >> account)
160 , Account.Pattern_Joker <$> (R.char '*' >> account_joker)
161 , Account.Pattern_Regex <$> (R.option '~' (R.char '~') >> account_regex)
164 -- * Parsing 'Amount'
166 -- | Parse an 'Amount'.
167 amount :: Stream s m Char => ParsecT s u m Amount
171 R.option Nothing $ do
173 s <- R.many $ R.space_horizontal
174 return $ Just $ (u, not $ null s)
175 (quantity_, style) <- do
182 , grouping_fractional
185 [ quantity '_' ',' '_' <* (R.notFollowedBy $ R.oneOf ",._")
186 , quantity '_' '.' '_' <* (R.notFollowedBy $ R.oneOf ",._")
187 , quantity ',' '.' '_' <* (R.notFollowedBy $ R.oneOf ",._")
188 , quantity '.' ',' '_' <* (R.notFollowedBy $ R.oneOf ",._")
190 let int = Data.List.concat integral
191 let frac_flat = Data.List.concat fractional
192 let frac = reverse $ Data.List.dropWhile (== '0') $ reverse $ frac_flat
193 let place = length frac
195 let mantissa = R.integer_of_digits 10 $ int ++ frac
197 ( Data.Decimal.Decimal
202 , Style.grouping_integral
203 , Style.grouping_fractional
204 , Style.precision = fromIntegral $ length frac_flat
207 (unit_, unit_side, unit_spaced) <-
210 return (u, Just Style.Side_Left, Just s)
212 R.option (Unit.nil, Nothing, Nothing) $ R.try $ do
213 s <- R.many $ R.space_horizontal
215 return $ (u, Just Style.Side_Right, Just $ not $ null s)
218 { Amount.quantity = left_signing $ quantity_
219 , Amount.style = style
223 , Amount.unit = unit_
228 { integral :: [String]
229 , fractional :: [String]
230 , fractioning :: Maybe Style.Fractioning
231 , grouping_integral :: Maybe Style.Grouping
232 , grouping_fractional :: Maybe Style.Grouping
235 -- | Parse a 'Quantity'.
238 => Char -- ^ Integral grouping separator.
239 -> Char -- ^ Fractioning separator.
240 -> Char -- ^ Fractional grouping separator.
241 -> ParsecT s u m Quantity
242 quantity int_group_sep frac_sep frac_group_sep = do
243 (integral, grouping_integral) <- do
246 [] -> return ([], Nothing)
248 t <- R.many $ R.char int_group_sep >> R.many1 R.digit
250 return (digits, grouping_of_digits int_group_sep digits)
251 (fractional, fractioning, grouping_fractional) <-
254 _ -> R.option ([], Nothing, Nothing)) $ do
255 fractioning <- R.char frac_sep
257 t <- R.many $ R.char frac_group_sep >> R.many1 R.digit
259 return (digits, Just fractioning
260 , grouping_of_digits frac_group_sep $ reverse digits)
267 , grouping_fractional
270 grouping_of_digits :: Char -> [String] -> Maybe Style.Grouping
271 grouping_of_digits group_sep digits =
276 Style.Grouping group_sep $
277 canonicalize_grouping $
279 canonicalize_grouping :: [Int] -> [Int]
280 canonicalize_grouping groups =
281 Data.List.foldl -- NOTE: remove duplicates at beginning and reverse.
282 (\acc l0 -> case acc of
283 l1:_ -> if l0 == l1 then acc else l0:acc
285 case groups of -- NOTE: keep only longer at beginning.
286 l0:l1:t -> if l0 > l1 then groups else l1:t
289 -- | Parse an 'Unit'.
290 unit :: Stream s m Char => ParsecT s u m Unit
292 (quoted <|> unquoted) <?> "unit"
294 unquoted :: Stream s m Char => ParsecT s u m Unit
299 case Data.Char.generalCategory c of
300 Data.Char.CurrencySymbol -> True
301 Data.Char.LowercaseLetter -> True
302 Data.Char.ModifierLetter -> True
303 Data.Char.OtherLetter -> True
304 Data.Char.TitlecaseLetter -> True
305 Data.Char.UppercaseLetter -> True
307 quoted :: Stream s m Char => ParsecT s u m Unit
310 R.between (R.char '"') (R.char '"') $
316 directive_alias :: Stream s m Char => ParsecT s Context m ()
318 _ <- R.string "alias"
319 R.skipMany1 $ R.space_horizontal
320 pattern <- account_pattern
321 R.skipMany $ R.space_horizontal
323 R.skipMany $ R.space_horizontal
325 R.skipMany $ R.space_horizontal
327 Account.Pattern_Exact acct -> R.modifyState $ \ctx -> ctx{context_aliases_exact=
328 Data.Map.insert acct repl $ context_aliases_exact ctx}
329 Account.Pattern_Joker jokr -> R.modifyState $ \ctx -> ctx{context_aliases_joker=
330 (jokr, repl):context_aliases_joker ctx}
331 Account.Pattern_Regex regx -> R.modifyState $ \ctx -> ctx{context_aliases_regex=
332 (regx, repl):context_aliases_regex ctx}
335 -- | Parse the year, month and day separator: '/' or '-'.
336 date_separator :: Stream s m Char => ParsecT s u m Char
337 date_separator = R.satisfy (\c -> c == '/' || c == '-')
339 -- | Parse the hour, minute and second separator: ':'.
340 hour_separator :: Stream s m Char => ParsecT s u m Char
341 hour_separator = R.char ':'
345 -- | Parse a 'Date' in [YYYY/]MM/DD [HH:MM[:SS][TZ]] format.
346 date :: Stream s m Char => Maybe Integer -> ParsecT s u m Date
348 n0 <- R.many1 R.digit
349 day_sep <- date_separator
350 n1 <- R.try (R.count 2 R.digit) <|> R.count 1 R.digit
351 n2 <- R.option Nothing $ R.try $ do
353 Just <$> do R.try (R.count 2 R.digit) <|> R.count 1 R.digit
355 case (n2, def_year) of
356 (Nothing, Nothing) -> fail $ show Error_year_or_day_is_missing
357 (Nothing, Just year) -> return (year, n0, n1)
358 (Just d, _) -> return (R.integer_of_digits 10 n0, n1, d)
359 let month = fromInteger $ R.integer_of_digits 10 m
360 let day = fromInteger $ R.integer_of_digits 10 d
361 guard $ month >= 1 && month <= 12
362 guard $ day >= 1 && day <= 31
363 day_ <- case Time.fromGregorianValid year month day of
364 Nothing -> fail $ show $ Error_invalid_day (year, month, day)
365 Just day_ -> return day_
366 (hour, minu, sec, tz) <-
367 R.option (0, 0, 0, Time.utc) $ R.try $ do
368 R.skipMany1 $ R.space_horizontal
369 hour <- R.try (R.count 2 R.digit) <|> R.count 1 R.digit
370 sep <- hour_separator
371 minu <- R.try (R.count 2 R.digit) <|> R.count 1 R.digit
372 sec <- R.option Nothing $ R.try $ do
374 Just <$> (R.try (R.count 2 R.digit) <|> R.count 1 R.digit)
376 tz <- R.option Time.utc $ R.try $ do
377 R.skipMany $ R.space_horizontal
380 ( R.integer_of_digits 10 hour
381 , R.integer_of_digits 10 minu
382 , maybe 0 (R.integer_of_digits 10) sec
385 guard $ hour >= 0 && hour <= 23
386 guard $ minu >= 0 && minu <= 59
387 guard $ sec >= 0 && sec <= 60 -- NOTE: allow leap second
389 tod <- case Time.makeTimeOfDayValid
393 Nothing -> fail $ show $ Error_invalid_time_of_day (hour, minu, sec)
394 Just tod -> return tod
397 (Time.LocalTime day_ tod)
401 time_zone :: Stream s m Char => ParsecT s u m TimeZone
403 -- DOC: http://www.timeanddate.com/time/zones/
404 -- TODO: only a few time zones are suported below.
405 -- TODO: check the timeZoneSummerOnly values
407 [ R.char 'A' >> R.choice
408 [ R.string "ST" >> return (TimeZone ((-4) * 60) False "AST")
409 , R.string "DT" >> return (TimeZone ((-3) * 60) True "ADT")
410 , return (TimeZone ((-1) * 60) False "A")
412 , R.char 'B' >> R.choice
413 [ R.string "ST" >> return (TimeZone ((-11) * 60) False "BST")
414 , R.string "DT" >> return (TimeZone ((-10) * 60) True "BDT")
416 , R.char 'C' >> R.choice
417 [ R.char 'E' >> R.choice
418 [ R.string "T" >> return (TimeZone ((1) * 60) True "CET")
419 , R.string "ST" >> return (TimeZone ((2) * 60) False "CEST")
421 , R.string "ST" >> return (TimeZone ((-6) * 60) False "CST")
422 , R.string "DT" >> return (TimeZone ((-5) * 60) True "CDT")
424 , R.char 'E' >> R.choice
425 [ R.string "ST" >> return (TimeZone ((-5) * 60) False "EST")
426 , R.string "DT" >> return (TimeZone ((-4) * 60) True "EDT")
428 , R.string "GMT" >> return (TimeZone 0 False "GMT")
429 , R.char 'H' >> R.choice
430 [ R.string "ST" >> return (TimeZone ((-10) * 60) False "HST")
431 , R.string "DT" >> return (TimeZone (( -9) * 60) True "HDT")
433 , R.char 'M' >> R.choice
434 [ R.string "ST" >> return (TimeZone ((-7) * 60) False "MST")
435 , R.string "DT" >> return (TimeZone ((-6) * 60) True "MDT")
436 , return (TimeZone ((-12) * 60) False "M")
438 , R.char 'N' >> R.choice
439 [ R.string "ST" >> return (TimeZone ((-3) * 60 - 30) False "NST")
440 , return (TimeZone (1 * 60) False "N")
442 , R.char 'P' >> R.choice
443 [ R.string "ST" >> return (TimeZone ((-8) * 60) False "PST")
444 , R.string "DT" >> return (TimeZone ((-7) * 60) True "PDT")
446 , R.char 'Y' >> R.choice
447 [ R.string "ST" >> return (TimeZone ((-9) * 60) False "YST")
448 , R.string "DT" >> return (TimeZone ((-8) * 60) True "YDT")
449 , return (TimeZone (12 * 60) False "Y")
451 , R.char 'Z' >> return (TimeZone 0 False "Z")
455 time_zone_digits :: Stream s m Char => ParsecT s u m TimeZone
456 {-# INLINEABLE time_zone_digits #-}
457 time_zone_digits = do
459 hour <- R.integer_of_digits 10 <$> R.count 2 R.digit
460 _ <- R.option ':' (R.char ':')
461 minute <- R.integer_of_digits 10 <$> R.count 2 R.digit
463 { timeZoneMinutes = sign_ (fromInteger hour * 60 + fromInteger minute)
464 , timeZoneSummerOnly = False
465 , timeZoneName = Time.timeZoneOffsetString tz
469 -- * Parsing 'Comment'
471 comment_begin :: Char
474 comment :: Stream s m Char => ParsecT s u m Comment
476 _ <- R.char comment_begin
478 R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
481 comments :: Stream s m Char => ParsecT s u m [Comment]
485 R.many1_separated comment (R.new_line >> R.skipMany R.space_horizontal)
491 tag_value_sep :: Char
498 tag :: Stream s m Char => ParsecT s u m Tag
501 _ <- R.char tag_value_sep
506 tag_name :: Stream s m Char => ParsecT s u m Tag.Name
509 R.many1 $ R.satisfy (\c -> c /= tag_value_sep && not (Data.Char.isSpace c))
511 tag_value :: Stream s m Char => ParsecT s u m Tag.Value
514 R.manyTill R.anyChar $ do
516 R.try (R.char tag_sep >> R.many R.space_horizontal >> tag_name >> R.char tag_value_sep >> return ())
520 tags :: Stream s m Char => ParsecT s u m Tag.By_Name
523 R.many_separated tag $ do
525 R.skipMany $ R.space_horizontal
528 not_tag :: Stream s m Char => ParsecT s u m ()
530 R.skipMany $ R.try $ do
531 R.skipMany $ R.satisfy
532 (\c -> c /= tag_value_sep
533 && not (Data.Char.isSpace c))
536 -- * Parsing 'Posting'
538 -- | Parse a 'Posting'.
539 posting :: Stream s m Char => ParsecT s Context m (Posting, Posting.Type)
542 sourcepos <- R.getPosition
543 R.skipMany1 $ R.space_horizontal
545 R.skipMany $ R.space_horizontal
547 let (type_, account_) = posting_type acct
551 _ <- R.count 2 R.space_horizontal
552 R.skipMany $ R.space_horizontal
554 if u == Unit.nil then id
556 Data.Map.adjust (\a ->
557 a{ Amount.style = s{Style.precision = Style.precision $ Amount.style a}
560 (context_unit_and_style ctx) .
561 Amount.from_List <$> do
562 R.many_separated amount $ do
563 R.skipMany $ R.space_horizontal
564 _ <- R.char amount_sep
565 R.skipMany $ R.space_horizontal
567 , return Data.Map.empty
569 R.skipMany $ R.space_horizontal
570 -- TODO: balance assertion
572 comments_ <- comments
573 let tags_ = tags_of_comments comments_
575 case Data.Map.lookup "date" tags_ of
578 let date2s = Data.Map.lookup "date2" tags_ -- NOTE: support hledger's date2
579 dates_ <- (flip mapM) (dates ++ fromMaybe [] date2s) $
580 R.runParserT (date (Just $ context_year ctx) <* R.eof) () ""
582 Left ko -> fail $ show ko
583 Right ok -> return ok
584 case (dates, date2s) of -- NOTE: put hledger's date2 at least in second position
586 return $ context_date ctx:dates_
588 return (Posting.Posting
589 { Posting.account=account_
590 , Posting.amounts=amounts_
591 , Posting.comments=comments_
592 , Posting.dates=dates_
593 , Posting.sourcepos=sourcepos
594 , Posting.status=status_
602 tags_of_comments :: [Comment] -> Tag.By_Name
604 Data.Map.unionsWith (++)
606 ( Data.Either.either (const Data.Map.empty) id
607 . R.runParser (not_tag >> tags <* R.eof) () "" )
609 status :: Stream s m Char => ParsecT s u m Transaction.Status
612 R.skipMany $ R.space_horizontal
613 _ <- (R.char '*' <|> R.char '!')
618 -- | Return the Posting.'Posting.Type' and stripped 'Account' of the given 'Account'.
619 posting_type :: Account -> (Posting.Type, Account)
621 fromMaybe (Posting.Type_Regular, acct) $ do
624 case Text.stripPrefix virtual_begin name of
627 Text.stripSuffix virtual_end name'
628 >>= return . Text.strip
629 guard $ not $ Text.null name''
630 Just (Posting.Type_Virtual, name'':|[])
633 Text.stripPrefix virtual_balanced_begin name
634 >>= Text.stripSuffix virtual_balanced_end
635 >>= return . Text.strip
636 guard $ not $ Text.null name'
637 Just (Posting.Type_Virtual_Balanced, name':|[])
638 first_name:|acct' -> do
639 let rev_acct' = Data.List.reverse acct'
640 let last_name = Data.List.head rev_acct'
641 case Text.stripPrefix virtual_begin first_name
642 >>= return . Text.stripStart of
643 Just first_name' -> do
645 Text.stripSuffix virtual_end last_name
646 >>= return . Text.stripEnd
647 guard $ not $ Text.null first_name'
648 guard $ not $ Text.null last_name'
650 ( Posting.Type_Virtual
652 Data.List.reverse (last_name':Data.List.tail rev_acct')
656 Text.stripPrefix virtual_balanced_begin first_name
657 >>= return . Text.stripStart
659 Text.stripSuffix virtual_balanced_end last_name
660 >>= return . Text.stripEnd
661 guard $ not $ Text.null first_name'
662 guard $ not $ Text.null last_name'
664 ( Posting.Type_Virtual_Balanced
666 Data.List.reverse (last_name':Data.List.tail rev_acct')
669 virtual_begin = Text.singleton posting_type_virtual_begin
670 virtual_end = Text.singleton posting_type_virtual_end
671 virtual_balanced_begin = Text.singleton posting_type_virtual_balanced_begin
672 virtual_balanced_end = Text.singleton posting_type_virtual_balanced_end
674 posting_type_virtual_begin :: Char
675 posting_type_virtual_begin = '('
676 posting_type_virtual_balanced_begin :: Char
677 posting_type_virtual_balanced_begin = '['
678 posting_type_virtual_end :: Char
679 posting_type_virtual_end = ')'
680 posting_type_virtual_balanced_end :: Char
681 posting_type_virtual_balanced_end = ']'
683 -- * Parsing 'Transaction'
686 :: (Stream s (R.Error Error m) Char, Monad m)
687 => ParsecT s Context (R.Error Error m) Transaction
689 sourcepos <- R.getPosition
695 _ -> return x <* R.new_line
696 date_ <- date (Just $ context_year ctx)
698 R.option [] $ R.try $ do
699 R.skipMany $ R.space_horizontal
701 R.skipMany $ R.space_horizontal
703 (date (Just $ context_year ctx)) $
705 R.many $ R.space_horizontal
707 >> (R.many $ R.space_horizontal)
708 R.skipMany $ R.space_horizontal
710 code_ <- R.option "" $ R.try code
711 R.skipMany $ R.space_horizontal
712 description_ <- description
713 R.skipMany $ R.space_horizontal
714 comments_after <- comments
716 Data.Map.unionWith (++)
717 (tags_of_comments comments_before)
718 (tags_of_comments comments_after)
720 (postings_unchecked, postings_not_regular) <-
721 ((Posting.from_List . Data.List.map fst) *** id) .
722 Data.List.partition ((Posting.Type_Regular ==) . snd) <$>
723 R.many1_separated posting R.new_line
724 let (virtual_postings, balanced_virtual_postings_unchecked) =
725 join (***) (Posting.from_List . Data.List.map fst) $
726 Data.List.partition ((Posting.Type_Virtual ==) . snd)
729 case snd $ Calc.Balance.infer_equilibrium postings_unchecked of
730 Left ko -> R.fail_with "transaction infer_equilibrium" $ Error_transaction_not_equilibrated ko
731 Right ok -> return ok
732 balanced_virtual_postings <-
733 case snd $ Calc.Balance.infer_equilibrium balanced_virtual_postings_unchecked of
734 Left ko -> R.fail_with "transaction infer_equilibrium" $ Error_virtual_transaction_not_equilibrated ko
735 Right ok -> return ok
737 Transaction.Transaction
738 { Transaction.code=code_
739 , Transaction.comments_before
740 , Transaction.comments_after
741 , Transaction.dates=(date_, dates_)
742 , Transaction.description=description_
743 , Transaction.postings
744 , Transaction.virtual_postings
745 , Transaction.balanced_virtual_postings
746 , Transaction.sourcepos
747 , Transaction.status=status_
748 , Transaction.tags=tags_
755 code :: Stream s m Char => ParsecT s Context m Transaction.Code
758 R.skipMany $ R.space_horizontal
759 R.between (R.char '(') (R.char ')') $
760 R.many $ R.satisfy (\c -> c /= ')' && not (R.is_space_horizontal c))
763 description :: Stream s m Char => ParsecT s u m Transaction.Description
766 R.many $ R.try description_char
769 description_char :: Stream s m Char => ParsecT s u m Char
770 description_char = do
773 _ | c == comment_begin -> R.parserZero
774 _ | R.is_space_horizontal c -> return c <* (R.lookAhead $ R.try $ description_char)
775 _ | not (Data.Char.isSpace c) -> return c
778 -- * Parsing directives
780 default_year :: Stream s m Char => ParsecT s Context m ()
782 year <- R.integer_of_digits 10 <$> R.many1 R.digit
783 R.skipMany R.space_horizontal >> R.new_line
784 context_ <- R.getState
785 R.setState context_{context_year=year}
788 default_unit_and_style :: Stream s m Char => ParsecT s Context m ()
789 default_unit_and_style = (do
791 R.skipMany R.space_horizontal >> R.new_line
792 context_ <- R.getState
793 R.setState context_{context_unit_and_style =
795 ( Amount.unit amount_
796 , Amount.style amount_ )}
797 ) <?> "default unit and style"
800 :: Stream s (R.Error Error IO) Char
801 => ParsecT s Context (R.Error Error IO) ()
803 sourcepos <- R.getPosition
804 filename <- R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
805 context_ <- R.getState
806 let journal_ = context_journal context_
807 let cwd = Path.takeDirectory (R.sourceName sourcepos)
808 file_path <- liftIO $ Path.abs cwd filename
810 liftIO $ Exception.catch
811 (liftM return $ readFile file_path)
812 (return . R.fail_with "include reading" . Error_reading_file file_path)
814 (journal_included, context_included) <- do
816 R.runParserT_with_Error (R.and_state $ journal_rec file_path)
817 context_{context_journal = Journal.nil}
820 Right ok -> return ok
821 Left ko -> R.fail_with "include parsing" $ Error_including_file file_path ko
823 context_included{context_journal=
824 journal_{Journal.includes=
825 journal_included{Journal.file=file_path}
826 : Journal.includes journal_}}
829 -- * Parsing 'Journal'
832 :: Stream s (R.Error Error IO) Char
834 -> ParsecT s Context (R.Error Error IO) Journal
836 currentLocalTime <- liftIO $
838 <$> Time.getCurrentTimeZone
839 <*> Time.getCurrentTime
840 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
841 context_ <- R.getState
842 R.setState $ context_{context_year=currentLocalYear}
847 :: Stream s (R.Error Error IO) Char
849 -> ParsecT s Context (R.Error Error IO) Journal
850 journal_rec file_ = do
851 last_read_time <- lift $ liftIO Time.getCurrentTime
854 [ R.skipMany1 R.space
856 [ R.string "Y" >> return default_year
857 , R.string "D" >> return default_unit_and_style
858 , R.string "!include" >> return include
860 >>= \r -> R.skipMany1 R.space_horizontal >> r)
863 context_' <- R.getState
864 let j = context_journal context_'
865 R.setState $ context_'{context_journal=
866 j{Journal.transactions=
867 Data.Map.insertWith (flip (++))
868 -- NOTE: flip-ing preserves order but slows down
869 -- when many transactions have the very same date.
870 (Date.to_UTC $ fst $ Transaction.dates t) [t]
871 (Journal.transactions j)}}
872 R.new_line <|> R.eof))
873 , R.try (comment >> return ())
876 journal_ <- context_journal <$> R.getState
879 { Journal.file = file_
880 , Journal.last_read_time
881 , Journal.includes = reverse $ Journal.includes journal_
884 -- ** Parsing 'Journal' from a file
886 file :: FilePath -> ExceptT (R.ParseError, [Error]) IO Journal
890 (liftM Right $ Text.IO.readFile path) $
891 \ko -> return $ Left $
892 ( R.newErrorUnknown $ R.initialPos path
893 , [Error_reading_file path ko]
895 >>= liftIO . R.runParserT_with_Error (journal path) nil_Context path
897 Left ko -> throwE $ ko
898 Right ok -> ExceptT $ return $ Right ok