1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE NamedFieldPuns #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE ScopedTypeVariables #-}
6 {-# LANGUAGE TupleSections #-}
7 {-# LANGUAGE TypeFamilies #-}
8 module Hcompta.JCC.Read where
10 import Prelude (Int, Integer, Num(..), fromIntegral)
11 import Control.Applicative ((<$>), (<*>), (<*))
13 import Data.Char (Char)
14 import qualified Data.Char as Char
16 import Data.Either (Either(..))
17 import Data.Eq (Eq(..))
18 import qualified Control.Exception.Safe as Exn
19 import qualified System.FilePath.Posix as FilePath
20 import Data.Function (($), (.), id, const)
21 import System.IO (IO, FilePath)
22 import qualified Data.List as List
23 import Data.List.NonEmpty (NonEmpty(..))
24 import qualified Data.List.NonEmpty as NonEmpty
25 import Data.Map.Strict (Map)
26 import qualified Data.Map.Strict as Map
27 import Data.Maybe (Maybe(..), fromMaybe, maybe)
28 import Control.Monad (Monad(..), guard, join, void)
29 import Control.Monad.IO.Class (liftIO)
30 import Control.Monad.Trans.Except (ExceptT(..), throwE)
31 import Data.Monoid (Monoid(..))
32 import Data.Ord (Ord(..))
33 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
34 import qualified Text.Parsec as R hiding
47 import qualified Text.Parsec.Combinator.CorrectSourcePosWithTab as R
48 import qualified Text.Parsec.Error.Custom as R
49 import qualified Text.Parsec.Pos as R
50 import Text.Show (Show)
51 import Data.String (String, fromString)
52 import Data.Text (Text)
53 import qualified Data.Text.IO as Text.IO (readFile)
54 import qualified Data.Time.Calendar as Time
55 import qualified Data.Time.Clock as Time
56 import Data.Time.LocalTime (TimeZone(..))
57 import qualified Data.Time.LocalTime as Time
58 import qualified Data.TreeMap.Strict as TreeMap
59 import Data.Typeable ()
61 import qualified Hcompta as H
62 import qualified Hcompta.JCC.Lib.FilePath as FilePath
63 import qualified Hcompta.JCC.Lib.Parsec as R
64 import Hcompta.Lib.Consable (Consable(..))
66 import Hcompta.JCC.Account
67 import Hcompta.JCC.Amount
68 import Hcompta.JCC.Chart
69 import Hcompta.JCC.Posting
70 import Hcompta.JCC.Transaction
71 import Hcompta.JCC.Journal
73 -- * Type 'Context_Read'
77 { context_read_account_prefix :: !(Maybe Account)
78 , context_read_aliases_exact :: !(Map Account Account)
79 , context_read_aliases_joker :: ![(Account_Joker, Account)]
80 -- , context_read_aliases_regex :: ![(Regex, Account)]
81 , context_read_cons :: Charted Transaction -> c
82 , context_read_date :: !H.Date
83 , context_read_journal :: !(Journal j)
84 , context_read_unit :: !(Maybe Unit)
85 , context_read_year :: !H.Year
90 => (Charted Transaction -> c)
93 context_read context_read_cons context_read_journal =
95 { context_read_account_prefix = Nothing
96 , context_read_aliases_exact = mempty
97 , context_read_aliases_joker = []
98 -- , context_read_aliases_regex = []
100 , context_read_date = H.date_epoch
101 , context_read_journal
102 , context_read_unit = Nothing
103 , context_read_year = H.date_year H.date_epoch
106 -- * Type 'Error_Read'
109 = Error_Read_account_anchor_unknown R.SourcePos H.Account_Anchor
110 | Error_Read_account_anchor_not_unique R.SourcePos H.Account_Anchor
111 | Error_Read_date Error_Read_Date
112 | Error_Read_transaction_not_equilibrated
116 , H.Balance_by_Unit_Sum Account_Section (H.Polarized Quantity)
118 | Error_Read_virtual_transaction_not_equilibrated
122 , H.Balance_by_Unit_Sum Account_Section (H.Polarized Quantity)
124 | Error_Read_reading_file FilePath Exn.IOException
125 | Error_Read_including_file FilePath [R.Error Error_Read]
128 -- * Read common patterns
130 is_space :: Char -> Bool
132 case Char.generalCategory c of
135 read_space :: Stream s m Char => ParsecT s u m Char
136 read_space = R.satisfy is_space
138 is_char :: Char -> Bool
140 case Char.generalCategory c of
141 Char.UppercaseLetter -> True
142 Char.LowercaseLetter -> True
143 Char.TitlecaseLetter -> True
144 Char.ModifierLetter -> True
145 Char.OtherLetter -> True
147 Char.NonSpacingMark -> True
148 Char.SpacingCombiningMark -> True
149 Char.EnclosingMark -> True
151 Char.DecimalNumber -> True
152 Char.LetterNumber -> True
153 Char.OtherNumber -> True
155 Char.ConnectorPunctuation -> True
156 Char.DashPunctuation -> True
157 Char.OpenPunctuation -> True
158 Char.ClosePunctuation -> True
159 Char.InitialQuote -> True
160 Char.FinalQuote -> True
161 Char.OtherPunctuation -> True
163 Char.MathSymbol -> True
164 Char.CurrencySymbol -> True
165 Char.ModifierSymbol -> True
166 Char.OtherSymbol -> True
169 Char.LineSeparator -> False
170 Char.ParagraphSeparator -> False
171 Char.Control -> False
173 Char.Surrogate -> False
174 Char.PrivateUse -> False
175 Char.NotAssigned -> False
176 read_char :: Stream s m Char => ParsecT s u m Char
177 read_char = R.satisfy is_char
179 is_char_active :: Char -> Bool
211 read_char_active :: Stream s m Char => ParsecT s u m Char
212 read_char_active = R.satisfy is_char_active
214 is_char_passive :: Char -> Bool
215 is_char_passive c = is_char c && not (is_char_active c)
216 read_char_passive :: Stream s m Char => ParsecT s u m Char
217 read_char_passive = R.satisfy is_char_passive
219 read_word :: Stream s m Char => ParsecT s u m Text
220 read_word = fromString <$> R.many read_char_passive
222 read_words :: Stream s m Char => ParsecT s u m [Text]
223 read_words = R.many_separated read_word read_space
225 read_name :: Stream s m Char => ParsecT s u m Text
226 read_name = fromString <$> R.many1 read_char_passive
228 read_tabulation :: Stream s m Char => ParsecT s u m Char
229 read_tabulation = R.char '\t'
231 read_hspace :: Stream s m Char => ParsecT s u m Char
232 read_hspace = R.char ' '
234 read_hspaces :: Stream s m Char => ParsecT s u m ()
235 read_hspaces = void $ R.many read_hspace
237 read_hspaces1 :: Stream s m Char => ParsecT s u m ()
238 read_hspaces1 = void $ R.many1 read_hspace
240 read_eol :: Stream s m Char => ParsecT s u m ()
241 read_eol = (<?> "eol") $
244 (void $ R.try $ R.string "\r\n")
246 read_line :: Stream s m Char => ParsecT s u m Text
247 read_line = fromString <$>
248 R.manyTill read_char (R.lookAhead read_eol <|> R.eof)
249 -- R.many (R.notFollowedBy eol >> char)
253 read_account :: Stream s m Char => ParsecT s u m Account
255 (H.account_from_List <$>) $
257 void $ R.char read_account_section_sep
260 read_account_section :: Stream s m Char => ParsecT s u m Text
261 read_account_section = read_name
263 read_account_section_sep :: Char
264 read_account_section_sep = '/'
266 read_comment_prefix :: Char
267 read_comment_prefix = ';'
269 read_account_section_joker :: Stream s m Char => ParsecT s u m Account_Joker_Section
270 read_account_section_joker = do
271 n <- R.option Nothing $ (Just <$> read_account_section)
273 Nothing -> R.char read_account_section_sep >> return Account_Joker_Any
274 Just n' -> return $ Account_Joker_Section n'
276 read_account_joker :: Stream s m Char => ParsecT s u m Account_Joker
277 read_account_joker = do
278 R.notFollowedBy $ R.spaceHorizontal
279 R.many1_separated read_account_section_joker $ R.char read_account_section_sep
282 read_account_regex :: Stream s m Char => ParsecT s u m Regex
283 read_account_regex = do
284 re <- R.many1 $ R.satisfy (not . R.is_space_horizontal)
288 read_account_pattern :: Stream s m Char => ParsecT s u m Account_Pattern
289 read_account_pattern =
291 [ Account_Pattern_Exact <$> (R.char '=' >> read_account)
292 , Account_Pattern_Joker <$> (R.char '*' >> read_account_joker)
293 -- , Account_Pattern_Regex <$> (R.option '~' (R.char '~') >> read_account_regex)
298 -- ** Read 'Account_Tag'
300 read_account_tag_prefix :: Char
301 read_account_tag_prefix = '.'
302 read_account_tag_sep :: Char
303 read_account_tag_sep = ':'
304 read_account_tag_value_prefix :: Char
305 read_account_tag_value_prefix = '='
307 read_account_tag :: Stream s m Char => ParsecT s u m H.Account_Tag
308 read_account_tag = (<?> "account_tag") $ do
309 void $ R.char read_account_tag_prefix
311 H.account_tag . (:|) p
312 <$> R.many (R.char read_account_tag_sep >> read_name)
315 (read_hspaces >> R.char read_transaction_tag_value_prefix >> read_hspaces >>
316 (List.concat <$> R.many (R.choice
317 [ R.string [read_account_tag_prefix , read_account_tag_prefix] >> return [read_account_tag_prefix]
318 , R.string [read_account_anchor_prefix, read_account_anchor_prefix] >> return [read_account_anchor_prefix]
319 , (\s c -> mappend s [c])
320 <$> R.many read_space
322 c /= read_account_tag_prefix &&
323 c /= read_account_anchor_prefix &&
327 -- ** Read 'Account_Anchor'
329 read_account_anchor_prefix :: Char
330 read_account_anchor_prefix = '~'
331 read_account_anchor_sep :: Char
332 read_account_anchor_sep = ':'
334 read_account_anchor :: Stream s m Char => ParsecT s u m H.Account_Anchor
335 read_account_anchor = (<?> "account_anchor") $ do
336 void $ R.char read_account_anchor_prefix
338 ps <- R.many (R.char read_account_anchor_sep >> read_name)
339 return $ H.account_anchor (p:|ps)
341 -- ** Read 'Account' 'Comment'
343 read_account_comment :: Stream s m Char => ParsecT s u m Comment
344 read_account_comment = read_comment
350 => Char -- ^ Integral grouping separator.
351 -> Char -- ^ Fractioning separator.
352 -> Char -- ^ Fractional grouping separator.
354 ( [String] -- integral
355 , [String] -- fractional
356 , Maybe Amount_Style_Fractioning -- fractioning
357 , Maybe Amount_Style_Grouping -- grouping_integral
358 , Maybe Amount_Style_Grouping -- grouping_fractional
360 read_quantity int_group_sep frac_sep frac_group_sep = do
361 (integral, grouping_integral) <- do
364 [] -> return ([], Nothing)
366 t <- R.many $ R.char int_group_sep >> R.many1 R.digit
368 return (digits, grouping_of_digits int_group_sep digits)
369 (fractional, fractioning, grouping_fractional) <-
372 _ -> R.option ([], Nothing, Nothing)) $ do
373 fractioning <- R.char frac_sep
375 t <- R.many $ R.char frac_group_sep >> R.many1 R.digit
377 return (digits, Just fractioning
378 , grouping_of_digits frac_group_sep $ List.reverse digits)
384 , grouping_fractional
387 grouping_of_digits :: Char -> [String] -> Maybe Amount_Style_Grouping
388 grouping_of_digits group_sep digits =
393 Amount_Style_Grouping group_sep $
394 canonicalize_grouping $
395 List.map List.length $ digits
396 canonicalize_grouping :: [Int] -> [Int]
397 canonicalize_grouping groups =
398 List.foldl' -- NOTE: remove duplicates at beginning and reverse.
399 (\acc l0 -> case acc of
400 l1:_ -> if l0 == l1 then acc else l0:acc
402 case groups of -- NOTE: keep only longer at beginning.
403 l0:l1:t -> if l0 > l1 then groups else l1:t
408 read_unit :: Stream s m Char => ParsecT s u m Unit
409 read_unit = (<?> "unit") $
412 unquoted :: Stream s m Char => ParsecT s u m Unit
417 case Char.generalCategory c of
418 Char.CurrencySymbol -> True
419 Char.LowercaseLetter -> True
420 Char.ModifierLetter -> True
421 Char.OtherLetter -> True
422 Char.TitlecaseLetter -> True
423 Char.UppercaseLetter -> True
425 quoted :: Stream s m Char => ParsecT s u m Unit
428 R.between (R.char '"') (R.char '"') $
436 => ParsecT s u m (Amount_Styled Amount)
437 read_amount = (<?> "amount") $ do
438 left_signing <- read_sign
440 R.option Nothing $ do
442 s <- R.many $ R.spaceHorizontal
443 return $ Just $ (u, not $ List.null s)
446 ( amount_style_integral
447 , amount_style_fractional
448 , amount_style_fractioning
449 , amount_style_grouping_integral
450 , amount_style_grouping_fractional
451 ) <- (<?> "quantity") $
453 [ read_quantity '_' ',' '_' <* R.notFollowedBy (R.oneOf ",._")
454 , read_quantity '_' '.' '_' <* R.notFollowedBy (R.oneOf ",._")
455 , read_quantity ',' '.' '_' <* R.notFollowedBy (R.oneOf ",._")
456 , read_quantity '.' ',' '_' <* R.notFollowedBy (R.oneOf ",._")
458 let int = List.concat amount_style_integral
459 let frac = List.concat amount_style_fractional
460 let precision = List.length frac
461 guard (precision <= 255)
462 let mantissa = R.integer_of_digits 10 $ int `mappend` frac
464 ( Data.Decimal.Decimal
465 (fromIntegral precision)
468 { amount_style_fractioning
469 , amount_style_grouping_integral
470 , amount_style_grouping_fractional
474 , amount_style_unit_side
475 , amount_style_unit_spaced ) <-
478 return (u, Just Amount_Style_Side_Left, Just s)
480 R.option (H.unit_empty, Nothing, Nothing) $ R.try $ do
481 s <- R.many R.spaceHorizontal
485 , Just Amount_Style_Side_Right
486 , Just $ not $ List.null s )
489 { amount_style_unit_side
490 , amount_style_unit_spaced
493 { amount_quantity = left_signing qty
498 -- | Parse either "-" into 'negate', or "+" or "" into 'id'.
499 read_sign :: (Stream s m Char, Num i) => ParsecT s u m (i -> i)
501 (R.char '-' >> return negate) <|>
502 (R.char '+' >> return id) <|>
508 = Error_Read_Date_year_or_day_is_missing
509 | Error_Read_Date_invalid_date (Integer, Int, Int)
510 | Error_Read_Date_invalid_time_of_day (Int, Int, Integer)
513 -- | Read a 'Date' in @[YYYY[/-]]MM[/-]DD[_HH:MM[:SS][TZ]]@ format.
515 :: (Stream s (R.State_Error e m) Char, Monad m)
516 => (Error_Read_Date -> e) -> Maybe Integer
517 -> ParsecT s u (R.State_Error e m) H.Date
518 read_date err def_year = (<?> "date") $ do
519 let read_2_or_1_digits = R.try (R.count 2 R.digit) <|> R.count 1 R.digit
520 n0 <- R.many1 R.digit
521 day_sep <- R.char read_date_ymd_sep
522 n1 <- read_2_or_1_digits
523 n2 <- R.option Nothing $ R.try $ do
524 void $ R.char day_sep
525 Just <$> read_2_or_1_digits
527 case (n2, def_year) of
528 (Nothing, Nothing) -> R.parserFailWith "date" (err $ Error_Read_Date_year_or_day_is_missing)
529 (Nothing, Just year) -> return (year, n0, n1)
530 (Just d, _) -> return (R.integer_of_digits 10 n0, n1, d)
531 let month = fromInteger $ R.integer_of_digits 10 m
532 let dom = fromInteger $ R.integer_of_digits 10 d
533 day <- case Time.fromGregorianValid year month dom of
534 Nothing -> R.parserFailWith "date" (err $ Error_Read_Date_invalid_date (year, month, dom))
535 Just day -> return day
536 (hour, minu, sec, tz) <-
537 R.option (0, 0, 0, Time.utc) $ R.try $ do
539 hour <- read_2_or_1_digits
540 sep <- R.char read_hour_separator
541 minu <- read_2_or_1_digits
542 sec <- R.option Nothing $ R.try $ do
544 Just <$> read_2_or_1_digits
545 tz <- R.option Time.utc $ R.try $
548 ( fromInteger $ R.integer_of_digits 10 hour
549 , fromInteger $ R.integer_of_digits 10 minu
550 , maybe 0 (R.integer_of_digits 10) sec
552 tod <- case Time.makeTimeOfDayValid hour minu (fromInteger sec) of
553 Nothing -> R.parserFailWith "date" $ err $
554 Error_Read_Date_invalid_time_of_day (hour, minu, sec)
555 Just tod -> return tod
556 return $ Time.localTimeToUTC tz (Time.LocalTime day tod)
558 -- | Separator for year, month and day: "-".
559 read_date_ymd_sep :: Char
560 read_date_ymd_sep = '-'
562 -- | Separator for hour, minute and second: ":".
563 read_hour_separator :: Char
564 read_hour_separator = ':'
566 read_time_zone :: Stream s m Char => ParsecT s u m TimeZone
568 -- DOC: http://www.timeanddate.com/time/zones/
569 -- TODO: only a few time zones are suported below.
570 -- TODO: check the timeZoneSummerOnly values
574 [ R.char 'A' >> R.choice
575 [ R.string "ST" >> return (TimeZone ((-4) * 60) False "AST")
576 , R.string "DT" >> return (TimeZone ((-3) * 60) True "ADT")
577 , return (TimeZone ((-1) * 60) False "A")
579 , R.char 'B' >> R.choice
580 [ R.string "ST" >> return (TimeZone ((-11) * 60) False "BST")
581 , R.string "DT" >> return (TimeZone ((-10) * 60) True "BDT")
583 , R.char 'C' >> R.choice
584 [ R.char 'E' >> R.choice
585 [ R.string "T" >> return (TimeZone ((1) * 60) True "CET")
586 , R.string "ST" >> return (TimeZone ((2) * 60) False "CEST")
588 , R.string "ST" >> return (TimeZone ((-6) * 60) False "CST")
589 , R.string "DT" >> return (TimeZone ((-5) * 60) True "CDT")
591 , R.char 'E' >> R.choice
592 [ R.string "ST" >> return (TimeZone ((-5) * 60) False "EST")
593 , R.string "DT" >> return (TimeZone ((-4) * 60) True "EDT")
595 , R.string "GMT" >> return (TimeZone 0 False "GMT")
596 , R.char 'H' >> R.choice
597 [ R.string "ST" >> return (TimeZone ((-10) * 60) False "HST")
598 , R.string "DT" >> return (TimeZone (( -9) * 60) True "HDT")
600 , R.char 'M' >> R.choice
601 [ R.string "ST" >> return (TimeZone ((-7) * 60) False "MST")
602 , R.string "DT" >> return (TimeZone ((-6) * 60) True "MDT")
603 , return (TimeZone ((-12) * 60) False "M")
605 , R.char 'N' >> R.choice
606 [ R.string "ST" >> return (TimeZone ((-3) * 60 - 30) False "NST")
607 , return (TimeZone (1 * 60) False "N")
609 , R.char 'P' >> R.choice
610 [ R.string "ST" >> return (TimeZone ((-8) * 60) False "PST")
611 , R.string "DT" >> return (TimeZone ((-7) * 60) True "PDT")
613 , R.char 'Y' >> R.choice
614 [ R.string "ST" >> return (TimeZone ((-9) * 60) False "YST")
615 , R.string "DT" >> return (TimeZone ((-8) * 60) True "YDT")
616 , return (TimeZone (12 * 60) False "Y")
618 , R.char 'Z' >> return (TimeZone 0 False "Z")
620 , read_time_zone_digits
623 read_time_zone_digits :: Stream s m Char => ParsecT s u m TimeZone
624 read_time_zone_digits = do
626 hour <- R.integer_of_digits 10 <$> R.count 2 R.digit
630 R.integer_of_digits 10 <$> R.count 2 R.digit
632 { timeZoneMinutes = sign_ (fromInteger hour * 60 + fromInteger minute)
633 , timeZoneSummerOnly = False
634 , timeZoneName = Time.timeZoneOffsetString tz
642 => ParsecT s u m Comment
643 read_comment = (<?> "comment") $ do
644 void $ R.char read_comment_prefix
646 R.manyTill R.anyChar (R.lookAhead (R.try read_eol <|> R.eof))
648 -- ** Read 'Comment's
652 => ParsecT s u m [Comment]
653 read_comments = (<?> "comments") $
657 R.many1_separated read_comment
658 (read_eol >> read_hspaces)
667 , Stream s (R.State_Error Error_Read m) Char
669 => ParsecT s (Context_Read c j)
670 (R.State_Error Error_Read m)
672 read_posting = (<?> "posting") $ do
673 posting_sourcepos <- R.getPosition
675 , posting_account_anchor ) <-
676 (<?> "posting_account") $
678 [ (,Nothing) <$> read_account
680 anchor <- read_account_anchor
682 let anchors = chart_anchors $
683 journal_chart $ context_read_journal ctx
684 case Map.lookup anchor anchors of
686 sa <- R.option Nothing $ Just <$> read_account
687 return $ ( a:|mappend as (maybe [] NonEmpty.toList sa)
688 , Just (anchor, sa) )
689 Nothing -> R.parserFailWith "account anchor" $
690 Error_Read_account_anchor_unknown posting_sourcepos anchor
693 R.option mempty $ R.try $ do
694 (style, amt) <- read_hspaces1 >> read_amount
695 ctx <- (<$> R.getState) $ \ctx ->
697 { context_read_journal=
698 let jnl = context_read_journal ctx in
700 { journal_amount_styles =
701 let Amount_Styles styles = journal_amount_styles jnl in
703 Map.insertWith mappend
711 case amount_unit amt of
712 u | u == H.unit_empty ->
713 fromMaybe u $ context_read_unit ctx
720 ) <- read_posting_attributes
723 , posting_account_anchor
725 , posting_anchors = H.Posting_Anchors posting_anchors
726 , posting_tags = H.Posting_Tags posting_tags
732 read_posting_attributes
733 :: Stream s (R.State_Error Error_Read m) Char
734 => ParsecT s (Context_Read c j)
735 (R.State_Error Error_Read m)
736 (H.Tags, H.Anchors, [Comment])
737 read_posting_attributes =
738 R.option mempty $ R.try $ do
739 void $ R.many (R.try (read_hspaces >> read_eol))
741 [ read_hspaces1 >> read_posting_anchor >>= \(H.Posting_Anchor p) -> do
742 (tags, H.Anchors anchors, cmts) <- read_posting_attributes
743 return (tags, H.Anchors (Map.insert p () anchors), cmts)
744 , read_hspaces1 >> read_posting_tag >>= \(H.Posting_Tag (p, v)) -> do
745 (H.Tags tags, anchors, cmts) <- read_posting_attributes
746 return (H.Tags (Map.insertWith mappend p [v] tags), anchors, cmts)
747 , read_hspaces >> read_comment >>= \c -> do
748 (tags, anchors, cmts) <- read_posting_attributes
749 return (tags, anchors, c:cmts)
752 read_amount_sep :: Char
753 read_amount_sep = '+'
755 read_posting_comment :: Stream s m Char => ParsecT s u m Comment
756 read_posting_comment = read_comment
758 -- ** Read 'Posting_Tag'
760 read_posting_tag :: Stream s m Char => ParsecT s u m H.Posting_Tag
761 read_posting_tag = (<?> "posting_tag") $
762 (<$> read_transaction_tag) $ \(H.Transaction_Tag t) ->
765 -- ** Read 'Posting_Anchor'
767 read_posting_anchor :: Stream s m Char => ParsecT s u m H.Posting_Anchor
768 read_posting_anchor = (<?> "posting_anchor") $ do
769 void $ R.char read_transaction_anchor_prefix
771 NonEmpty.fromList <$>
772 R.many1 (R.char read_transaction_anchor_sep >> read_name)
774 -- * Read 'Transaction'
779 , Stream s (R.State_Error Error_Read m) Char
781 => ParsecT s (Context_Read c j)
782 (R.State_Error Error_Read m)
784 read_transaction = (<?> "transaction") $ do
785 transaction_sourcepos <- R.getPosition
787 date_ <- read_date Error_Read_date (Just $ context_read_year ctx)
789 R.option [] $ R.try $ do
791 void $ R.char read_transaction_date_sep
794 (read_date Error_Read_date (Just $ context_read_year ctx)) $
797 >> R.char read_transaction_date_sep
799 let transaction_dates = (date_, dates_)
801 transaction_wording <- read_transaction_wording
803 , transaction_anchors
804 , transaction_comments
805 ) <- read_transaction_attributes
806 transaction_postings_unchecked <-
807 postings_by_account <$> read_postings
808 let transaction_unchecked =
810 { transaction_anchors = H.Transaction_Anchors transaction_anchors
811 , transaction_tags = H.Transaction_Tags transaction_tags
812 , transaction_comments
814 , transaction_wording
815 , transaction_postings = transaction_postings_unchecked
816 , transaction_sourcepos
818 let styles = journal_amount_styles $ context_read_journal ctx
819 transaction_postings <-
820 case H.balance_infer_equilibrium transaction_postings_unchecked of
821 (_, Left ko) -> R.parserFailWith "transaction infer_equilibrium" $
822 Error_Read_transaction_not_equilibrated styles transaction_unchecked ko
823 (_bal, Right ok) -> return ok
825 transaction_unchecked
826 { transaction_postings
829 read_transaction_attributes
830 :: Stream s (R.State_Error Error_Read m) Char
831 => ParsecT s (Context_Read c j)
832 (R.State_Error Error_Read m)
833 (H.Tags, H.Anchors, [Comment])
834 read_transaction_attributes =
835 R.option mempty $ R.try $ do
836 void $ R.many (R.try (read_hspaces >> read_eol))
838 [ read_hspaces1 >> read_transaction_anchor >>= \(H.Transaction_Anchor p) -> do
839 (tags, H.Anchors anchors, cmts) <- read_transaction_attributes
840 return (tags, H.Anchors (Map.insert p () anchors), cmts)
841 , read_hspaces1 >> read_transaction_tag >>= \(H.Transaction_Tag (p, v)) -> do
842 (H.Tags tags, anchors, cmts) <- read_transaction_attributes
843 return (H.Tags (Map.insertWith mappend p [v] tags), anchors, cmts)
844 , read_hspaces >> read_comment >>= \c -> do
845 (tags, anchors, cmts) <- read_transaction_attributes
846 return (tags, anchors, c:cmts)
850 (Consable c j, Monad m, Stream s (R.State_Error Error_Read m) Char)
851 => ParsecT s (Context_Read c j) (R.State_Error Error_Read m) [Posting]
852 read_postings = R.many $ R.try (read_hspaces >> read_eol >> read_hspaces1 >> read_posting)
854 read_transaction_date_sep :: Char
855 read_transaction_date_sep = '='
857 read_transaction_wording
859 => ParsecT s u m Wording
860 read_transaction_wording =
862 read_transaction_tag_value
864 -- ** Read 'Transaction_Anchor'
866 read_transaction_anchor_prefix :: Char
867 read_transaction_anchor_prefix = '@'
868 read_transaction_anchor_sep :: Char
869 read_transaction_anchor_sep = ':'
871 read_transaction_anchor :: Stream s m Char => ParsecT s u m H.Transaction_Anchor
872 read_transaction_anchor = (<?> "transaction_anchor") $ do
873 void $ R.char read_transaction_anchor_prefix
875 H.transaction_anchor . (:|) p
876 <$> R.many (R.char read_transaction_anchor_sep >> read_name)
878 -- ** Read 'Transaction_Tag'
880 read_transaction_tag_prefix :: Char
881 read_transaction_tag_prefix = '#'
882 read_transaction_tag_sep :: Char
883 read_transaction_tag_sep = ':'
884 read_transaction_tag_value_prefix :: Char
885 read_transaction_tag_value_prefix = '='
887 read_transaction_tag :: Stream s m Char => ParsecT s u m H.Transaction_Tag
888 read_transaction_tag = (<?> "transaction_tag") $ do
889 void $ R.char read_transaction_tag_prefix
891 H.transaction_tag . (:|) p
892 <$> R.many (R.char read_transaction_tag_sep >> read_name)
893 <*> R.option "" (R.try $ do
895 void $ R.char read_transaction_tag_value_prefix
897 read_transaction_tag_value)
899 read_transaction_tag_value
901 => ParsecT s u m H.Tag_Value
902 read_transaction_tag_value =
903 (fromString . List.concat <$>) $
905 s <- R.many read_hspace
906 c <- R.satisfy $ \c ->
907 c /= read_transaction_tag_prefix &&
908 c /= read_transaction_anchor_prefix &&
909 c /= read_comment_prefix &&
911 cs <- R.many (R.satisfy is_char)
912 return $ mappend s (c:cs)
914 -- ** Read 'Transaction' 'Comment'
916 read_transaction_comment :: Stream s m Char => ParsecT s u m Comment
917 read_transaction_comment = read_comment
922 :: (Consable c j, Stream s m Char)
923 => ParsecT s (Context_Read c j) m ()
924 read_directive_alias = do
925 void $ R.string "alias"
926 R.skipMany1 R.spaceHorizontal
927 pat <- read_account_pattern
934 Account_Pattern_Exact acct ->
935 R.modifyState $ \ctx -> ctx{context_read_aliases_exact=
936 Map.insert acct repl $ context_read_aliases_exact ctx}
937 Account_Pattern_Joker jokr ->
938 R.modifyState $ \ctx -> ctx{context_read_aliases_joker=
939 (jokr, repl):context_read_aliases_joker ctx}
940 -- Account_Pattern_Regex regx ->
941 -- R.modifyState $ \ctx -> ctx{context_read_aliases_regex=
942 -- (regx, repl):context_read_aliases_regex ctx}
946 :: (Consable c j, Stream s m Char)
947 => ParsecT s (Context_Read c j) m ()
948 read_default_year = (<?> "default year") $ do
949 year <- R.integer_of_digits 10 <$> R.many1 R.digit
951 context_read_ <- R.getState
952 R.setState context_read_{context_read_year=year}
954 read_default_unit_and_style
957 => ParsecT s (Context_Read c j) m ()
958 read_default_unit_and_style = (<?> "default_unit_and_style") $ do
959 (sty, amt) <- read_amount
962 let unit = H.amount_unit amt
964 { context_read_journal =
965 let jnl = context_read_journal ctx in
967 { journal_amount_styles =
968 let Amount_Styles styles =
969 journal_amount_styles jnl in
971 Map.insertWith const unit sty styles
973 , context_read_unit = Just unit
979 , Stream s (R.State_Error Error_Read IO) Char
980 ) => ParsecT s (Context_Read c j) (R.State_Error Error_Read IO) ()
981 read_include = (<?> "include") $ do
982 sourcepos <- R.getPosition
983 filename <- R.manyTill R.anyChar (R.lookAhead (R.try read_eol <|> R.eof))
984 context_read_including <- R.getState
985 let journal_including = context_read_journal context_read_including
986 let cwd = FilePath.takeDirectory (R.sourceName sourcepos)
987 journal_file <- liftIO $ FilePath.path_absolute cwd filename
989 join $ liftIO $ Exn.catch
990 (return <$> Text.IO.readFile journal_file)
991 (return . R.parserFailWith "include reading" . Error_Read_reading_file journal_file)
992 (journal_included, context_read_included) <- do
994 R.runParserTWithError
995 (R.and_state $ read_journal_rec journal_file)
996 context_read_including
997 { context_read_journal =
999 { journal_chart = journal_chart journal_including
1000 , journal_amount_styles = journal_amount_styles journal_including
1003 journal_file content
1005 Right ok -> return ok
1006 Left ko -> R.parserFailWith "include parsing" $
1007 Error_Read_including_file journal_file ko
1009 context_read_included
1010 { context_read_journal =
1012 { journal_includes =
1013 journal_included{ journal_files = [journal_file] } :
1014 journal_includes journal_including
1016 journal_chart journal_included
1017 , journal_amount_styles =
1018 journal_amount_styles journal_included
1026 , Stream s (R.State_Error Error_Read IO) Char
1028 => ParsecT s (Context_Read c j)
1029 (R.State_Error Error_Read IO)
1031 read_chart = (<?> "chart") $ do
1032 -- sourcepos <- R.getPosition
1033 acct <- read_account
1037 , _chart_comments ) <-
1038 fields acct mempty mempty mempty
1039 let chart_accounts =
1040 TreeMap.singleton acct $
1041 H.Account_Tags chart_tags
1043 let j = context_read_journal ctx
1045 ctx{context_read_journal=
1063 [ read_hspaces1 >> read_account_comment >>= \c ->
1064 fields acct tags anchors (c:cmts)
1065 , read_hspaces1 >> read_account_tag >>= \(H.Account_Tag (p, v)) ->
1066 fields acct (H.Tags $ Map.insertWith mappend p [v] tagm) anchors cmts
1067 , read_hspaces1 >> read_account_anchor >>= \anchor ->
1068 case Map.insertLookupWithKey (\_k n _o -> n) anchor acct anchors of
1069 (Nothing, m) -> fields acct tags m cmts
1071 sourcepos <- R.getPosition
1072 R.parserFailWith "account anchor not unique"
1073 (Error_Read_account_anchor_not_unique sourcepos anchor)
1074 , read_hspaces >> read_eol >>
1075 fields acct tags anchors cmts
1076 , return (tags, anchors, cmts)
1084 , Stream s (R.State_Error Error_Read IO) Char
1087 -> ParsecT s (Context_Read c j)
1088 (R.State_Error Error_Read IO)
1090 read_journal filepath = (<?> "journal") $ do
1091 currentLocalTime <- liftIO $
1093 <$> Time.getCurrentTimeZone
1094 <*> Time.getCurrentTime
1095 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
1097 R.setState $ ctx{context_read_year=currentLocalYear}
1098 read_journal_rec filepath
1103 , Stream s (R.State_Error Error_Read IO) Char
1106 -> ParsecT s (Context_Read c j)
1107 (R.State_Error Error_Read IO)
1109 read_journal_rec journal_file = do
1110 last_read_time <- liftIO H.date_now
1118 journal_ <- context_read_journal <$> R.getState
1121 { journal_files = [journal_file]
1122 , journal_includes = List.reverse $ journal_includes journal_
1123 , journal_last_read_time = last_read_time
1128 => ParsecT s u m (ParsecT s u m ())
1131 R.skipMany (read_hspaces >> read_eol)
1133 R.skipMany (read_hspaces >> read_eol)
1134 R.try (read_hspaces >> R.eof) <|> loop r
1138 , u ~ Context_Read c j
1139 , m ~ R.State_Error Error_Read IO
1141 => ParsecT s u m (ParsecT s u m ())
1144 void $ R.lookAhead (R.try $ R.char read_comment_prefix)
1146 _cmts <- read_comments
1148 R.modifyState $ \ctx ->
1149 let j = context_read_journal ctx in
1150 ctx{context_read_journal=
1152 mcons (context_read_filter ctx) cmts $
1160 , u ~ Context_Read c j
1161 , m ~ R.State_Error Error_Read IO
1163 => ParsecT s u m (ParsecT s u m ())
1164 jump_directive = (<?> "directive") $ do
1165 let choice s = R.string s >> R.skipMany1 R.spaceHorizontal
1167 [ choice "Y" >> return read_default_year
1168 , choice "D" >> return read_default_unit_and_style
1169 , choice "!include" >> return read_include
1174 , u ~ Context_Read c j
1175 , m ~ R.State_Error Error_Read IO
1177 => ParsecT s u m (ParsecT s u m ())
1178 jump_transaction = do
1179 void $ R.lookAhead $ R.try (R.many1 R.digit >> R.char read_date_ymd_sep)
1181 t <- read_transaction
1182 R.modifyState $ \ctx ->
1183 let j = context_read_journal ctx in
1184 ctx{context_read_journal=
1187 (context_read_cons ctx $
1188 Charted (journal_chart j) t)
1189 (journal_content j)}}
1193 , u ~ Context_Read c j
1194 , m ~ R.State_Error Error_Read IO
1196 => ParsecT s u m (ParsecT s u m ())
1203 :: (Consable c j, Monoid j)
1206 -> ExceptT [R.Error Error_Read] IO (Journal j)
1207 read_file ctx path =
1210 (Right <$> Text.IO.readFile path) $
1211 \ko -> return $ Left $
1212 [R.Error_Custom (R.initialPos path) $
1213 Error_Read_reading_file path ko])
1214 >>= liftIO . R.runParserTWithError
1215 (read_journal path) ctx path
1217 Left ko -> throwE $ ko
1218 Right ok -> ExceptT $ return $ Right ok