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 qualified Data.Char
16 import qualified Data.Decimal
17 import qualified Data.Either
18 import qualified Data.List
19 import qualified Data.Map.Strict as Data.Map
20 import Data.Maybe (fromMaybe)
21 import qualified Data.Time.Calendar as Time
22 import qualified Data.Time.Clock as Time
23 import qualified Data.Time.LocalTime as Time
24 import Data.Time.LocalTime (TimeZone(..))
25 import Data.Typeable ()
26 import qualified Text.Parsec as R
27 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
28 import qualified Data.Text.IO as Text.IO (readFile)
29 import qualified Data.Text as Text (pack)
30 import qualified System.FilePath.Posix as Path
32 import qualified Hcompta.Model.Account as Account
33 import Hcompta.Model.Account (Account)
34 import qualified Hcompta.Model.Amount as Amount
35 import Hcompta.Model.Amount (Amount)
36 import qualified Hcompta.Model.Amount.Style as Style
37 import qualified Hcompta.Model.Amount.Unit as Unit
38 import Hcompta.Model.Amount.Unit (Unit)
39 import qualified Hcompta.Model.Transaction as Transaction
40 import Hcompta.Model.Transaction (Transaction, Comment)
41 import qualified Hcompta.Model.Transaction.Posting as Posting
42 import Hcompta.Model.Transaction (Posting)
43 import qualified Hcompta.Model.Transaction.Tag as Tag
44 import Hcompta.Model.Transaction (Tag)
45 import qualified Hcompta.Model.Date as Date
46 import Hcompta.Model.Date (Date)
47 import Hcompta.Format.Ledger.Journal as Journal
48 import qualified Hcompta.Lib.Regex as Regex
49 import Hcompta.Lib.Regex (Regex)
50 import Hcompta.Lib.Parsec as R
51 import qualified Hcompta.Lib.Path as Path
55 { context_account_prefix :: !Account
56 , context_aliases_exact :: !(Data.Map.Map Account Account)
57 , context_aliases_joker :: ![(Account.Joker, Account)]
58 , context_aliases_regex :: ![(Regex, Account)]
59 , context_date :: !Date
60 , context_unit_and_style :: !(Maybe (Amount.Unit, Amount.Style))
61 , context_journal :: !Journal
62 , context_year :: !Date.Year
65 nil_Context :: Context
68 { context_account_prefix = []
69 , context_aliases_exact = Data.Map.empty
70 , context_aliases_joker = []
71 , context_aliases_regex = []
72 , context_date = Date.nil
73 , context_unit_and_style = Nothing
74 , context_journal = Journal.nil
75 , context_year = (\(year, _ , _) -> year) $
76 Time.toGregorian $ Time.utctDay $
77 Journal.last_read_time Journal.nil
80 -- | Parse either '-' into 'negate', or '+' or '' into 'id'.
81 sign :: (Stream s m Char, Num i) => ParsecT s u m (i -> i)
83 (R.char '-' >> return negate)
84 <|> (R.char '+' >> return id)
87 -- * Parsing 'Account'
89 account_name_sep :: Char
90 account_name_sep = ':'
92 -- | Parse an 'Account'.
93 account :: Stream s m Char => ParsecT s u m Account
95 R.notFollowedBy $ space_horizontal
96 many1_separated account_name $ R.char account_name_sep
98 -- | Parse an Account.'Account.Name'.
99 account_name :: Stream s m Char => ParsecT s u m Account.Name
102 R.many1 $ R.try account_name_char
104 account_name_char :: Stream s m Char => ParsecT s u m Char
105 account_name_char = do
108 _ | c == comment_begin -> R.parserZero
109 _ | c == account_name_sep -> R.parserZero
110 _ | c == posting_type_virtual_end
111 || c == posting_type_virtual_balanced_end ->
112 return c <* (R.lookAhead $ R.try $ account_name_char)
113 _ | is_space_horizontal c -> do
114 _ <- R.notFollowedBy $ space_horizontal
115 return c <* (R.lookAhead $ R.try $
116 ( R.try (R.char account_name_sep)
117 <|> account_name_char
119 _ | not (Data.Char.isSpace c) -> return c
122 -- | Parse an Account.'Account.Joker_Name'.
123 account_joker_name :: Stream s m Char => ParsecT s u m Account.Joker_Name
124 account_joker_name = do
125 n <- R.option Nothing $ (Just <$> account_name)
127 Nothing -> R.char account_name_sep >> (return $ Account.Joker_Any)
128 Just n' -> return $ Account.Joker_Name n'
130 -- | Parse an Account.'Account.Joker'.
131 account_joker :: Stream s m Char => ParsecT s u m Account.Joker
133 R.notFollowedBy $ space_horizontal
134 many1_separated account_joker_name $ R.char account_name_sep
136 -- | Parse a 'Regex'.
137 account_regex :: Stream s m Char => ParsecT s u m Regex
139 re <- R.many1 $ R.satisfy (not . is_space_horizontal)
142 -- | Parse an Account.'Account.Filter'.
143 account_pattern :: Stream s m Char => ParsecT s u m Account.Pattern
146 [ Account.Pattern_Exact <$> (R.char '=' >> account)
147 , Account.Pattern_Joker <$> (R.char '*' >> account_joker)
148 , Account.Pattern_Regex <$> (R.option '~' (R.char '~') >> account_regex)
151 -- * Parsing 'Amount'
153 -- | Parse an 'Amount'.
154 amount :: Stream s m Char => ParsecT s u m Amount
158 R.option Nothing $ do
160 s <- R.many $ space_horizontal
161 return $ Just $ (u, not $ null s)
162 (quantity_, style) <- do
169 , grouping_fractional
172 [ quantity '_' ',' '_' <* (R.notFollowedBy $ R.oneOf ",._")
173 , quantity '_' '.' '_' <* (R.notFollowedBy $ R.oneOf ",._")
174 , quantity ',' '.' '_' <* (R.notFollowedBy $ R.oneOf ",._")
175 , quantity '.' ',' '_' <* (R.notFollowedBy $ R.oneOf ",._")
177 let int = Data.List.concat integral
178 let frac_flat = Data.List.concat fractional
179 let frac = reverse $ Data.List.dropWhile (== '0') $ reverse $ frac_flat
180 let place = length frac
182 let mantissa = R.integer_of_digits 10 $ int ++ frac
184 ( Data.Decimal.Decimal
189 , Style.grouping_integral
190 , Style.grouping_fractional
191 , Style.precision = fromIntegral $ length frac_flat
194 (unit_, unit_side, unit_spaced) <-
197 return (u, Just Style.Side_Left, Just s)
199 R.option (Unit.nil, Nothing, Nothing) $ do
200 s <- R.many $ space_horizontal
202 return $ (u, Just Style.Side_Right, Just $ not $ null s)
205 { Amount.quantity = left_signing $ quantity_
206 , Amount.style = style
210 , Amount.unit = unit_
215 { integral :: [String]
216 , fractional :: [String]
217 , fractioning :: Maybe Style.Fractioning
218 , grouping_integral :: Maybe Style.Grouping
219 , grouping_fractional :: Maybe Style.Grouping
222 -- | Parse a 'Quantity'.
225 => Char -- ^ Integral grouping separator.
226 -> Char -- ^ Fractioning separator.
227 -> Char -- ^ Fractional grouping separator.
228 -> ParsecT s u m Quantity
229 quantity int_group_sep frac_sep frac_group_sep = do
230 (integral, grouping_integral) <- do
233 [] -> return ([], Nothing)
235 t <- R.many $ R.char int_group_sep >> R.many1 R.digit
237 return (digits, grouping_of_digits int_group_sep digits)
238 (fractional, fractioning, grouping_fractional) <-
241 _ -> R.option ([], Nothing, Nothing)) $ do
242 fractioning <- R.char frac_sep
244 t <- R.many $ R.char frac_group_sep >> R.many1 R.digit
246 return (digits, Just fractioning
247 , grouping_of_digits frac_group_sep $ reverse digits)
254 , grouping_fractional
257 grouping_of_digits :: Char -> [String] -> Maybe Style.Grouping
258 grouping_of_digits group_sep digits =
263 Style.Grouping group_sep $
264 canonicalize_grouping $
266 canonicalize_grouping :: [Int] -> [Int]
267 canonicalize_grouping groups =
268 Data.List.foldl -- NOTE: remove duplicates at beginning and reverse.
269 (\acc l0 -> case acc of
270 l1:_ -> if l0 == l1 then acc else l0:acc
272 case groups of -- NOTE: keep only longer at beginning.
273 l0:l1:t -> if l0 > l1 then groups else l1:t
276 -- | Parse an 'Unit'.
277 unit :: Stream s m Char => ParsecT s u m Unit
279 (quoted <|> unquoted) <?> "unit"
281 unquoted :: Stream s m Char => ParsecT s u m Unit
286 case Data.Char.generalCategory c of
287 Data.Char.CurrencySymbol -> True
288 Data.Char.LowercaseLetter -> True
289 Data.Char.ModifierLetter -> True
290 Data.Char.OtherLetter -> True
291 Data.Char.TitlecaseLetter -> True
292 Data.Char.UppercaseLetter -> True
294 quoted :: Stream s m Char => ParsecT s u m Unit
297 R.between (R.char '"') (R.char '"') $
303 directive_alias :: Stream s m Char => ParsecT s Context m ()
305 _ <- R.string "alias"
306 R.skipMany1 $ space_horizontal
307 pattern <- account_pattern
308 R.skipMany $ space_horizontal
310 R.skipMany $ space_horizontal
312 R.skipMany $ space_horizontal
314 Account.Pattern_Exact acct -> R.modifyState $ \ctx -> ctx{context_aliases_exact=
315 Data.Map.insert acct repl $ context_aliases_exact ctx}
316 Account.Pattern_Joker jokr -> R.modifyState $ \ctx -> ctx{context_aliases_joker=
317 (jokr, repl):context_aliases_joker ctx}
318 Account.Pattern_Regex regx -> R.modifyState $ \ctx -> ctx{context_aliases_regex=
319 (regx, repl):context_aliases_regex ctx}
322 -- | Parse the year, month and day separator: '/' or '-'.
323 date_separator :: Stream s m Char => ParsecT s u m Char
324 date_separator = R.satisfy (\c -> c == '/' || c == '-')
326 -- | Parse the hour, minute and second separator: ':'.
327 hour_separator :: Stream s m Char => ParsecT s u m Char
328 hour_separator = R.char ':'
332 -- | Parse a 'Date' in [YYYY/]MM/DD [HH:MM[:SS][TZ]] format.
333 date :: Stream s m Char => Maybe Integer -> ParsecT s u m Date
335 n0 <- R.many1 R.digit
336 day_sep <- date_separator
337 n1 <- R.try (R.count 2 R.digit) <|> R.count 1 R.digit
338 n2 <- R.option Nothing $ R.try $ do
340 Just <$> do R.try (R.count 2 R.digit) <|> R.count 1 R.digit
342 case (n2, def_year) of
343 (Nothing, Nothing) -> fail "year or day is missing"
344 (Nothing, Just year) -> return (year, n0, n1)
345 (Just d, _) -> return (R.integer_of_digits 10 n0, n1, d)
346 let month = fromInteger $ R.integer_of_digits 10 m
347 let day = fromInteger $ R.integer_of_digits 10 d
348 guard $ month >= 1 && month <= 12
349 guard $ day >= 1 && day <= 31
350 day_ <- case Time.fromGregorianValid year month day of
351 Nothing -> fail "invalid day"
352 Just day_ -> return day_
353 (hour, minu, sec, tz) <-
354 R.option (0, 0, 0, Time.utc) $ R.try $ do
355 R.skipMany1 $ space_horizontal
356 hour <- R.try (R.count 2 R.digit) <|> R.count 1 R.digit
357 sep <- hour_separator
358 minu <- R.try (R.count 2 R.digit) <|> R.count 1 R.digit
359 sec <- R.option Nothing $ R.try $ do
361 Just <$> (R.try (R.count 2 R.digit) <|> R.count 1 R.digit)
363 tz <- R.option Time.utc $ R.try $ do
364 R.skipMany $ space_horizontal
367 ( R.integer_of_digits 10 hour
368 , R.integer_of_digits 10 minu
369 , maybe 0 (R.integer_of_digits 10) sec
371 guard $ hour >= 0 && hour <= 23
372 guard $ minu >= 0 && minu <= 59
373 guard $ sec >= 0 && sec <= 60 -- NOTE: allow leap second
374 tod <- case Time.makeTimeOfDayValid
378 Nothing -> fail "invalid time of day"
379 Just tod -> return tod
382 (Time.LocalTime day_ tod)
386 time_zone :: Stream s m Char => ParsecT s u m TimeZone
388 -- DOC: http://www.timeanddate.com/time/zones/
389 -- TODO: only a few time zones are suported below.
390 -- TODO: check the timeZoneSummerOnly values
392 [ R.char 'A' >> R.choice
393 [ R.string "ST" >> return (TimeZone ((-4) * 60) False "AST")
394 , R.string "DT" >> return (TimeZone ((-3) * 60) True "ADT")
395 , return (TimeZone ((-1) * 60) False "A")
397 , R.char 'B' >> R.choice
398 [ R.string "ST" >> return (TimeZone ((-11) * 60) False "BST")
399 , R.string "DT" >> return (TimeZone ((-10) * 60) True "BDT")
401 , R.char 'C' >> R.choice
402 [ R.char 'E' >> R.choice
403 [ R.string "T" >> return (TimeZone ((1) * 60) True "CET")
404 , R.string "ST" >> return (TimeZone ((2) * 60) False "CEST")
406 , R.string "ST" >> return (TimeZone ((-6) * 60) False "CST")
407 , R.string "DT" >> return (TimeZone ((-5) * 60) True "CDT")
409 , R.char 'E' >> R.choice
410 [ R.string "ST" >> return (TimeZone ((-5) * 60) False "EST")
411 , R.string "DT" >> return (TimeZone ((-4) * 60) True "EDT")
413 , R.string "GMT" >> return (TimeZone 0 False "GMT")
414 , R.char 'H' >> R.choice
415 [ R.string "ST" >> return (TimeZone ((-10) * 60) False "HST")
416 , R.string "DT" >> return (TimeZone (( -9) * 60) True "HDT")
418 , R.char 'M' >> R.choice
419 [ R.string "ST" >> return (TimeZone ((-7) * 60) False "MST")
420 , R.string "DT" >> return (TimeZone ((-6) * 60) True "MDT")
421 , return (TimeZone ((-12) * 60) False "M")
423 , R.char 'N' >> R.choice
424 [ R.string "ST" >> return (TimeZone ((-3) * 60 - 30) False "NST")
425 , return (TimeZone (1 * 60) False "N")
427 , R.char 'P' >> R.choice
428 [ R.string "ST" >> return (TimeZone ((-8) * 60) False "PST")
429 , R.string "DT" >> return (TimeZone ((-7) * 60) True "PDT")
431 , R.char 'Y' >> R.choice
432 [ R.string "ST" >> return (TimeZone ((-9) * 60) False "YST")
433 , R.string "DT" >> return (TimeZone ((-8) * 60) True "YDT")
434 , return (TimeZone (12 * 60) False "Y")
436 , R.char 'Z' >> return (TimeZone 0 False "Z")
440 time_zone_digits :: Stream s m Char => ParsecT s u m TimeZone
441 {-# INLINEABLE time_zone_digits #-}
442 time_zone_digits = do
444 hour <- R.integer_of_digits 10 <$> R.count 2 R.digit
445 _ <- R.option ':' (R.char ':')
446 minute <- R.integer_of_digits 10 <$> R.count 2 R.digit
448 { timeZoneMinutes = sign_ (fromInteger hour * 60 + fromInteger minute)
449 , timeZoneSummerOnly = False
450 , timeZoneName = Time.timeZoneOffsetString tz
454 -- * Parsing 'Comment'
456 comment_begin :: Char
459 comment :: Stream s m Char => ParsecT s u m Comment
461 _ <- R.char comment_begin
463 R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
466 comments :: Stream s m Char => ParsecT s u m [Comment]
469 R.skipMany $ R.satisfy Data.Char.isSpace
470 many1_separated comment $
473 R.try space_horizontal
474 <|> (R.new_line >> space_horizontal)
479 tag_value_sep :: Char
486 tag :: Stream s m Char => ParsecT s u m Tag
489 _ <- R.char tag_value_sep
494 tag_name :: Stream s m Char => ParsecT s u m Tag.Name
497 R.many1 $ R.satisfy (\c -> c /= tag_value_sep && not (Data.Char.isSpace c))
499 tag_value :: Stream s m Char => ParsecT s u m Tag.Value
502 R.manyTill R.anyChar $ do
504 R.try (R.char tag_sep >> R.many space_horizontal >> tag_name >> R.char tag_value_sep >> return ())
508 tags :: Stream s m Char => ParsecT s u m Tag.By_Name
511 R.many_separated tag $ do
513 R.skipMany $ space_horizontal
516 not_tag :: Stream s m Char => ParsecT s u m ()
518 R.skipMany $ R.try $ do
519 R.skipMany $ R.satisfy
520 (\c -> c /= tag_value_sep
521 && not (Data.Char.isSpace c))
524 -- * Parsing 'Posting'
526 -- | Parse a 'Posting'.
527 posting :: Stream s m Char => ParsecT s Context m (Posting, Posting.Type)
530 sourcepos <- R.getPosition
531 comments_ <- comments
532 R.skipMany1 $ space_horizontal
534 R.skipMany $ space_horizontal
535 (account_, type_) <- account_with_posting_type
539 _ <- R.count 2 space_horizontal
540 R.skipMany $ space_horizontal
541 Amount.from_List <$> do
542 R.many_separated amount $ R.try $ do
543 R.skipMany $ space_horizontal
544 _ <- R.char amount_sep
545 R.skipMany $ space_horizontal
547 , return Data.Map.empty
549 R.skipMany $ space_horizontal
550 -- TODO: balance assertion
552 comments__ <- (comments_ ++) <$> comments
553 let tags_ = tags_of_comments comments__
555 case Data.Map.lookup "date" tags_ of
558 let date2s = Data.Map.lookup "date2" tags_ -- NOTE: support hledger's date2
559 dates_ <- (flip mapM) (dates ++ fromMaybe [] date2s) $
560 R.runParserT (date (Just $ context_year ctx) <* R.eof) () ""
562 Left ko -> fail $ show ko
563 Right ok -> return ok
564 case (dates, date2s) of -- NOTE: put hledger's date2 at least in second position
566 return $ context_date ctx:dates_
568 return (Posting.Posting
569 { Posting.account=account_
570 , Posting.amounts=amounts_
571 , Posting.comments=comments__
572 , Posting.dates=dates_
573 , Posting.sourcepos=sourcepos
574 , Posting.status=status_
582 tags_of_comments :: [Comment] -> Tag.By_Name
584 Data.Map.unionsWith (++)
586 ( Data.Either.either (const Data.Map.empty) id
587 . R.runParser (not_tag >> tags <* R.eof) () "" )
589 status :: Stream s m Char => ParsecT s u m Transaction.Status
592 R.skipMany $ space_horizontal
593 _ <- (R.char '*' <|> R.char '!') <?> "status"
598 -- | Parse an 'Account' with Posting.'Posting.Type'.
599 account_with_posting_type :: Stream s m Char => ParsecT s u m (Account, Posting.Type)
600 account_with_posting_type = do
602 [ (, Posting.Type_Virtual) <$> R.between (R.char posting_type_virtual_begin)
603 (R.char posting_type_virtual_end)
605 , (, Posting.Type_Virtual_Balanced) <$> R.between (R.char posting_type_virtual_balanced_begin)
606 (R.char posting_type_virtual_balanced_end)
608 , (, Posting.Type_Regular) <$> account
611 posting_type_virtual_begin :: Char
612 posting_type_virtual_begin = '('
613 posting_type_virtual_balanced_begin :: Char
614 posting_type_virtual_balanced_begin = '['
615 posting_type_virtual_end :: Char
616 posting_type_virtual_end = ')'
617 posting_type_virtual_balanced_end :: Char
618 posting_type_virtual_balanced_end = ']'
620 -- * Parsing 'Transaction'
622 transaction :: Stream s m Char => ParsecT s Context m Transaction
624 sourcepos <- R.getPosition
626 comments_before <- comments
627 date_ <- date (Just $ context_year ctx)
629 R.option [] $ R.try $ do
630 R.skipMany $ space_horizontal
632 R.skipMany $ space_horizontal
634 (date (Just $ context_year ctx)) $
636 R.many $ space_horizontal
638 >> (R.many $ space_horizontal)
639 R.skipMany $ space_horizontal
641 code_ <- R.option "" $ R.try code
642 R.skipMany $ space_horizontal
643 description_ <- description
644 R.skipMany $ space_horizontal
645 comments_after <- comments
647 Data.Map.unionWith (++)
648 (tags_of_comments comments_before)
649 (tags_of_comments comments_after)
651 postings_ <- many1_separated posting R.new_line
652 let (postings, postings__) =
653 (Posting.from_List . Data.List.map fst) *** id $
655 ((Posting.Type_Regular ==) . snd)
657 let (virtual_postings, balanced_virtual_postings) =
658 join (***) (Posting.from_List . Data.List.map fst) $
660 ((Posting.Type_Virtual ==) . snd)
663 Transaction.Transaction
664 { Transaction.code=code_
665 , Transaction.comments_before
666 , Transaction.comments_after
667 , Transaction.dates=(date_, dates_)
668 , Transaction.description=description_
669 , Transaction.postings
670 , Transaction.virtual_postings
671 , Transaction.balanced_virtual_postings
672 , Transaction.sourcepos
673 , Transaction.status=status_
674 , Transaction.tags=tags_
681 code :: Stream s m Char => ParsecT s Context m Transaction.Code
684 R.skipMany $ space_horizontal
685 R.between (R.char '(') (R.char ')') $
686 R.many $ R.satisfy (\c -> c /= ')' && not (is_space_horizontal c))
689 description :: Stream s m Char => ParsecT s u m Transaction.Description
692 R.many $ R.try description_char
695 description_char :: Stream s m Char => ParsecT s u m Char
696 description_char = do
699 _ | c == comment_begin -> R.parserZero
700 _ | is_space_horizontal c -> return c <* (R.lookAhead $ R.try $ description_char)
701 _ | not (Data.Char.isSpace c) -> return c
704 -- * Parsing directives
706 default_year :: Stream s m Char => ParsecT s Context m ()
708 year <- R.integer_of_digits 10 <$> R.many1 R.digit
709 context_ <- R.getState
710 R.setState context_{context_year=year}
712 default_unit_and_style :: Stream s m Char => ParsecT s Context m ()
713 default_unit_and_style = do
714 R.skipMany1 space_horizontal
716 R.skipMany space_horizontal >> R.new_line >> R.skipMany space_horizontal
717 context_ <- R.getState
718 R.setState context_{context_unit_and_style=Just $
719 ( Amount.unit amount_
720 , Amount.style amount_ )}
722 include :: Stream s IO Char => ParsecT s Context IO ()
724 sourcepos <- R.getPosition
725 R.skipMany1 $ space_horizontal
726 filename <- R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
727 context_ <- R.getState
728 let journal_ = context_journal context_
729 let cwd = Path.takeDirectory (R.sourceName sourcepos)
730 file_ <- liftIO $ Path.abs cwd filename
731 (journal_included, context_included) <- liftIO $
734 (\ko -> fail $ concat -- TODO: i18n by using a custom data type
738 , ":\n", show (ko::Exception.IOException)
740 >>= R.runParserT (R.and_state $ journal_rec file_)
741 context_{context_journal = Journal.nil}
744 Left ko -> fail $ show ko
745 Right ok -> return ok
747 context_included{context_journal=
748 journal_{Journal.includes=
749 journal_included{Journal.file=file_}
750 : Journal.includes journal_}}
753 -- * Parsing 'Journal'
755 journal :: Stream s IO Char => FilePath -> ParsecT s Context IO Journal
757 currentLocalTime <- liftIO $
759 <$> Time.getCurrentTimeZone
760 <*> Time.getCurrentTime
761 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
762 context_ <- R.getState
763 R.setState $ context_{context_year=currentLocalYear}
767 journal_rec :: Stream s IO Char => FilePath -> ParsecT s Context IO Journal
768 journal_rec file_ = do
769 last_read_time <- liftIO $ Time.getCurrentTime
773 [ R.string "Y" >> return default_year
774 , R.string "D" >> return default_unit_and_style
775 , R.string "!include" >> return include
776 ] <?> "directive") >>= id)
779 context_' <- R.getState
780 let j = context_journal context_'
781 R.setState $ context_'{context_journal=
782 j{Journal.transactions=
783 Data.Map.insertWith (flip (++))
784 -- NOTE: flip-ing preserves order but slows down
785 -- when many transactions have the very same date.
786 (Date.to_UTC $ fst $ Transaction.dates t) [t]
787 (Journal.transactions j)}}
790 R.skipMany $ R.satisfy Data.Char.isSpace
792 journal_ <- context_journal <$> R.getState
795 { Journal.file = file_
796 , Journal.last_read_time
797 , Journal.includes = reverse $ Journal.includes journal_
800 -- ** Parsing 'Journal' from a file
802 file :: FilePath -> ExceptT String IO Journal
806 (liftM Right $ Text.IO.readFile path) $
807 \ko -> return $ Left $ show (ko::Exception.IOException)
808 >>= liftIO . R.runParserT (journal path) nil_Context path
810 Left ko -> throwE $ show ko
811 Right ok -> ExceptT $ return $ Right ok