1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE NamedFieldPuns #-}
3 {-# LANGUAGE OverloadedStrings #-}
4 {-# LANGUAGE ScopedTypeVariables #-}
5 {-# LANGUAGE TupleSections #-}
6 {-# LANGUAGE TypeFamilies #-}
7 module Hcompta.Format.Ledger.Read where
9 -- import Control.Applicative ((<$>), (<*>), (<*))
10 import Control.Arrow ((***), first)
11 import qualified Control.Exception as Exception
12 import Control.Monad (guard, liftM, join, forM, void)
13 import Control.Monad.IO.Class (liftIO)
14 import Control.Monad.Trans.Except (ExceptT(..), throwE)
15 import qualified Data.Char
16 import qualified Data.Either
17 import qualified Data.List
18 import qualified Data.List.NonEmpty as NonEmpty
19 import Data.List.NonEmpty (NonEmpty(..))
20 import qualified Data.Map.Strict as Data.Map
21 import Data.Map.Strict (Map)
22 import Data.Maybe (fromMaybe)
23 import Data.String (fromString)
24 import qualified Data.Text as Text
25 import qualified Data.Time.Calendar as Time
26 import qualified Data.Time.Clock as Time
27 import qualified Data.Time.LocalTime as Time
28 import Data.Typeable ()
29 import qualified Text.Parsec as R hiding
42 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
43 import qualified Text.Parsec.Pos as R
44 import qualified Data.Text.IO as Text.IO (readFile)
45 import qualified System.FilePath.Posix as Path
47 import qualified Hcompta.Account as Account
48 import Hcompta.Account (Account)
49 import qualified Hcompta.Account.Read as Account.Read
50 import qualified Hcompta.Balance as Balance
51 import qualified Hcompta.Amount as Amount
52 import qualified Hcompta.Amount.Style as Style
53 import qualified Hcompta.Amount.Read as Amount.Read
54 import qualified Hcompta.Amount.Unit as Unit
55 import qualified Hcompta.Date as Date
56 import Hcompta.Date (Date)
57 import qualified Hcompta.Date.Read as Date.Read
58 import qualified Hcompta.Format.Ledger as Ledger
59 import Hcompta.Posting as Posting
60 import Hcompta.Format.Ledger
66 import Hcompta.Lib.Consable (Consable(..))
67 import Hcompta.Lib.Regex (Regex)
68 import qualified Hcompta.Lib.Parsec as R
69 import qualified Hcompta.Lib.Path as Path
70 import qualified Hcompta.Tag as Tag
71 import Hcompta.Tag (Tag)
75 { context_account_prefix :: !(Maybe Account)
76 , context_aliases_exact :: !(Data.Map.Map Account Account)
77 , context_aliases_joker :: ![(Account.Joker, Account)]
78 , context_aliases_regex :: ![(Regex, Account)]
79 , context_date :: !Date
80 , context_filter :: !f
81 , context_journal :: !(Journal (ts t))
82 , context_unit_and_style :: !(Maybe (Amount.Unit, Amount.Style))
83 , context_year :: !Date.Year
87 :: (Show f, Consable f ts t)
88 => f -> Journal (ts t) -> Context f ts t
89 context flt context_journal =
91 { context_account_prefix = Nothing
92 , context_aliases_exact = Data.Map.empty
93 , context_aliases_joker = []
94 , context_aliases_regex = []
95 , context_date = Date.nil
96 , context_filter = flt
98 , context_unit_and_style = Nothing
99 , context_year = Date.year Date.nil
103 = Error_date Date.Read.Error
104 | Error_transaction_not_equilibrated Transaction [Balance.Unit_Sum (Balance.Posting_Amount Ledger.Posting)]
105 | Error_virtual_transaction_not_equilibrated Transaction [Balance.Unit_Sum (Balance.Posting_Amount Ledger.Posting)]
106 | Error_reading_file FilePath Exception.IOException
107 | Error_including_file FilePath [R.Error Error]
113 :: (Consable f ts t, Stream s m Char)
114 => ParsecT s (Context f ts t) m ()
116 _ <- R.string "alias"
117 R.skipMany1 $ R.space_horizontal
118 pattern <- Account.Read.pattern
119 R.skipMany $ R.space_horizontal
121 R.skipMany $ R.space_horizontal
122 repl <- Account.Read.account
123 R.skipMany $ R.space_horizontal
125 Account.Pattern_Exact acct -> R.modifyState $ \ctx -> ctx{context_aliases_exact=
126 Data.Map.insert acct repl $ context_aliases_exact ctx}
127 Account.Pattern_Joker jokr -> R.modifyState $ \ctx -> ctx{context_aliases_joker=
128 (jokr, repl):context_aliases_joker ctx}
129 Account.Pattern_Regex regx -> R.modifyState $ \ctx -> ctx{context_aliases_regex=
130 (regx, repl):context_aliases_regex ctx}
135 comment_begin :: Char
138 comment :: Stream s m Char => ParsecT s u m Comment
140 _ <- R.char comment_begin
142 R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
145 comments :: Stream s m Char => ParsecT s u m [Comment]
149 R.many1_separated comment (R.new_line >> R.skipMany R.space_horizontal)
155 tag_value_sep :: Char
161 tag_path_section_char :: Stream s m Char => ParsecT s u m Char
162 tag_path_section_char =
163 R.satisfy (\c -> c /= tag_value_sep && c /= tag_sep && not (Data.Char.isSpace c))
165 tag :: Stream s m Char => ParsecT s u m Tag
166 tag = ((,) <$> tag_path <*> tag_value) <?> "tag"
168 tag_path :: Stream s m Char => ParsecT s u m Tag.Path
170 NonEmpty.fromList <$> do
171 R.many1 $ R.try tag_path_section
173 tag_path_section :: Stream s m Char => ParsecT s u m Tag.Section
174 tag_path_section = do
176 ((R.many1 $ tag_path_section_char) <* R.char tag_value_sep)
178 tag_value :: Stream s m Char => ParsecT s u m Tag.Value
181 R.manyTill R.anyChar $ do
183 R.try (R.char tag_sep >> R.many R.space_horizontal >> void tag_path_section)
184 <|> R.try (void (R.try R.new_line))
187 tags :: Stream s m Char => ParsecT s u m (Map Tag.Path [Tag.Value])
189 Data.Map.fromListWith (flip (++))
190 . map (\(p, v) -> (p, [v])) <$> do
191 R.many_separated tag $ do
193 R.skipMany $ R.space_horizontal
195 not_tag :: Stream s m Char => ParsecT s u m ()
197 R.skipMany $ R.try $ do
198 R.skipMany $ tag_path_section_char
204 :: (Consable f ts t, Stream s (R.Error_State Error m) Char, Monad m)
205 => ParsecT s (Context f ts t) (R.Error_State Error m) (Posting, Posting.Posting_Type)
208 sourcepos <- R.getPosition
209 R.skipMany1 $ R.space_horizontal
211 R.skipMany $ R.space_horizontal
212 acct <- Account.Read.account
213 let (type_, account_) = posting_type acct
217 (void R.tab <|> void (R.count 2 R.space_horizontal))
218 R.skipMany $ R.space_horizontal
220 if u == Unit.nil then id
222 Data.Map.adjust (\a ->
223 a{ Amount.style = s{Style.precision = Style.precision $ Amount.style a}
226 (context_unit_and_style ctx) .
227 Amount.from_List <$> do
228 R.many_separated Amount.Read.amount $ do
229 R.skipMany $ R.space_horizontal
230 _ <- R.char amount_sep
231 R.skipMany $ R.space_horizontal
233 , return Data.Map.empty
235 R.skipMany $ R.space_horizontal
236 -- TODO: balance assertion
238 comments_ <- comments
239 let tags_ = tags_of_comments comments_
241 case Data.Map.lookup ("date":|[]) tags_ of
244 let date2s = Data.Map.lookup ("date2":|[]) tags_ -- NOTE: support hledger's date2
246 forM (dates ++ fromMaybe [] date2s) $ \s ->
247 R.runParserT_with_Error_fail "tag date" id
248 (Date.Read.date Error_date (Just $ context_year ctx) <* R.eof) ()
250 >>= \dates_ -> case (dates, date2s) of -- NOTE: put hledger's date2 at least in second position
252 return $ context_date ctx:dates_
255 { posting_account=account_
256 , posting_amounts=amounts_
257 , posting_comments=comments_
258 , posting_dates=dates_
259 , posting_sourcepos=sourcepos
260 , posting_status=status_
268 tags_of_comments :: [Comment] -> Map Tag.Path [Tag.Value]
270 Data.Map.unionsWith (++)
272 ( Data.Either.either (const Data.Map.empty) id
273 . R.runParser (not_tag >> tags <* R.eof) () "" )
275 status :: Stream s m Char => ParsecT s u m Ledger.Status
278 R.skipMany $ R.space_horizontal
279 _ <- (R.char '*' <|> R.char '!')
284 -- | Return the 'Posting_Type' and stripped 'Account' of the given 'Account'.
285 posting_type :: Account -> (Posting_Type, Account)
287 fromMaybe (Posting_Type_Regular, acct) $ do
290 case Text.stripPrefix virtual_begin name of
292 name'' <- liftM Text.strip $ Text.stripSuffix virtual_end name'
293 guard $ not $ Text.null name''
294 Just (Posting_Type_Virtual, name'':|[])
296 name' <- liftM Text.strip $
297 Text.stripPrefix virtual_balanced_begin name
298 >>= Text.stripSuffix virtual_balanced_end
299 guard $ not $ Text.null name'
300 Just (Posting_Type_Virtual_Balanced, name':|[])
301 first_name:|acct' -> do
302 let rev_acct' = Data.List.reverse acct'
303 let last_name = Data.List.head rev_acct'
304 case liftM Text.stripStart $
305 Text.stripPrefix virtual_begin first_name of
306 Just first_name' -> do
307 last_name' <- liftM Text.stripEnd $
308 Text.stripSuffix virtual_end last_name
309 guard $ not $ Text.null first_name'
310 guard $ not $ Text.null last_name'
312 ( Posting_Type_Virtual
314 Data.List.reverse (last_name':Data.List.tail rev_acct')
317 first_name' <- liftM Text.stripStart $
318 Text.stripPrefix virtual_balanced_begin first_name
319 last_name' <- liftM Text.stripEnd $
320 Text.stripSuffix virtual_balanced_end last_name
321 guard $ not $ Text.null first_name'
322 guard $ not $ Text.null last_name'
324 ( Posting_Type_Virtual_Balanced
326 Data.List.reverse (last_name':Data.List.tail rev_acct')
329 virtual_begin = Text.singleton posting_type_virtual_begin
330 virtual_end = Text.singleton posting_type_virtual_end
331 virtual_balanced_begin = Text.singleton posting_type_virtual_balanced_begin
332 virtual_balanced_end = Text.singleton posting_type_virtual_balanced_end
334 posting_type_virtual_begin :: Char
335 posting_type_virtual_begin = '('
336 posting_type_virtual_balanced_begin :: Char
337 posting_type_virtual_balanced_begin = '['
338 posting_type_virtual_end :: Char
339 posting_type_virtual_end = ')'
340 posting_type_virtual_balanced_end :: Char
341 posting_type_virtual_balanced_end = ']'
343 -- * Read 'Transaction'
346 :: (Consable f ts t, Stream s (R.Error_State Error m) Char, Monad m)
347 => ParsecT s (Context f ts t) (R.Error_State Error m) Transaction
350 transaction_sourcepos <- R.getPosition
351 transaction_comments_before <-
355 _ -> return x <* R.new_line
356 date_ <- Date.Read.date Error_date (Just $ context_year ctx)
358 R.option [] $ R.try $ do
359 R.skipMany $ R.space_horizontal
361 R.skipMany $ R.space_horizontal
363 (Date.Read.date Error_date (Just $ context_year ctx)) $
365 R.many $ R.space_horizontal
367 >> (R.many $ R.space_horizontal)
368 let transaction_dates = (date_, dates_)
369 R.skipMany $ R.space_horizontal
370 transaction_status <- status
371 transaction_code <- R.option "" $ R.try code
372 R.skipMany $ R.space_horizontal
373 transaction_description <- description
374 R.skipMany $ R.space_horizontal
375 transaction_comments_after <- comments
376 let transaction_tags =
377 Data.Map.unionWith (++)
378 (tags_of_comments transaction_comments_before)
379 (tags_of_comments transaction_comments_after)
381 (postings_unchecked, postings_not_regular) <-
382 first (Ledger.posting_by_Account . Data.List.map fst) .
383 Data.List.partition ((Posting.Posting_Type_Regular ==) . snd) <$>
384 R.many1_separated posting R.new_line
385 let (transaction_virtual_postings, balanced_virtual_postings_unchecked) =
386 join (***) (Ledger.posting_by_Account . Data.List.map fst) $
387 Data.List.partition ((Posting.Posting_Type_Virtual ==) . snd)
392 , transaction_comments_before
393 , transaction_comments_after
395 , transaction_description
396 , transaction_postings=postings_unchecked
397 , transaction_virtual_postings
398 , transaction_balanced_virtual_postings=balanced_virtual_postings_unchecked
399 , transaction_sourcepos
403 transaction_postings <-
404 case Balance.infer_equilibrium postings_unchecked of
405 (_, Left ko) -> R.fail_with "transaction infer_equilibrium"
406 (Error_transaction_not_equilibrated tr_unchecked ko)
407 (_bal, Right ok) -> return ok
408 transaction_balanced_virtual_postings <-
409 case Balance.infer_equilibrium balanced_virtual_postings_unchecked of
410 (_, Left ko) -> R.fail_with "transaction infer_equilibrium"
411 (Error_virtual_transaction_not_equilibrated tr_unchecked ko)
412 (_bal, Right ok) -> return ok
415 { transaction_postings
416 , transaction_balanced_virtual_postings
423 code :: (Consable f ts t, Stream s m Char)
424 => ParsecT s (Context f ts t) m Ledger.Code
427 R.skipMany $ R.space_horizontal
428 R.between (R.char '(') (R.char ')') $
429 R.many $ R.satisfy (\c -> c /= ')' && not (R.is_space_horizontal c))
432 description :: Stream s m Char => ParsecT s u m Ledger.Description
435 R.many $ R.try description_char
438 description_char :: Stream s m Char => ParsecT s u m Char
439 description_char = do
442 _ | c == comment_begin -> R.parserZero
443 _ | R.is_space_horizontal c -> return c <* (R.lookAhead $ R.try $ description_char)
444 _ | not (Data.Char.isSpace c) -> return c
450 :: (Consable f ts t, Stream s m Char)
451 => ParsecT s (Context f ts t) m ()
453 year <- R.integer_of_digits 10 <$> R.many1 R.digit
454 R.skipMany R.space_horizontal >> R.new_line
455 context_ <- R.getState
456 R.setState context_{context_year=year}
459 default_unit_and_style
460 :: (Consable f ts t, Stream s m Char)
461 => ParsecT s (Context f ts t) m ()
462 default_unit_and_style = (do
463 amount_ <- Amount.Read.amount
464 R.skipMany R.space_horizontal >> R.new_line
465 context_ <- R.getState
466 R.setState context_{context_unit_and_style =
468 ( Amount.unit amount_
469 , Amount.style amount_ )}
470 ) <?> "default unit and style"
473 ( Consable f ts Transaction
475 , Show (ts Transaction)
476 , Stream s (R.Error_State Error IO) Char
478 => ParsecT s (Context f ts Transaction) (R.Error_State Error IO) ()
480 sourcepos <- R.getPosition
481 filename <- R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
482 context_ <- R.getState
483 let journal_ = context_journal context_
484 let cwd = Path.takeDirectory (R.sourceName sourcepos)
485 file_path <- liftIO $ Path.abs cwd filename
487 join $ liftIO $ Exception.catch
488 (liftM return $ readFile file_path)
489 (return . R.fail_with "include reading" . Error_reading_file file_path)
490 (journal_included, context_included) <- do
492 R.runParserT_with_Error (R.and_state $ journal_rec file_path)
493 context_{context_journal = Ledger.journal}
496 Right ok -> return ok
497 Left ko -> R.fail_with "include parsing" (Error_including_file file_path ko)
499 context_included{context_journal=
500 journal_{journal_includes=
501 journal_included{journal_file=file_path}
502 : journal_includes journal_}}
508 ( Consable f ts Transaction
510 , Show (ts Transaction)
511 , Stream s (R.Error_State Error IO) Char
514 -> ParsecT s (Context f ts Transaction) (R.Error_State Error IO) (Journal (ts Transaction))
516 currentLocalTime <- liftIO $
518 <$> Time.getCurrentTimeZone
519 <*> Time.getCurrentTime
520 let (currentLocalYear, _, _) = Time.toGregorian $ Time.localDay currentLocalTime
522 R.setState $ ctx{context_year=currentLocalYear}
527 ( Consable f ts Transaction
529 , Show (ts Transaction)
530 , Stream s (R.Error_State Error IO) Char
533 -> ParsecT s (Context f ts Transaction) (R.Error_State Error IO) (Journal (ts Transaction))
534 journal_rec file_ = do
535 last_read_time <- liftIO Date.now
538 [ R.skipMany1 R.space
540 [ R.string "Y" >> return default_year
541 , R.string "D" >> return default_unit_and_style
542 , R.string "!include" >> return include
544 >>= \r -> R.skipMany1 R.space_horizontal >> r)
548 let j = context_journal ctx
551 j{journal_transactions=
552 mcons (context_filter ctx) t $
553 journal_transactions j}}
554 R.new_line <|> R.eof))
555 , R.try (void $ comment)
558 journal_ <- context_journal <$> R.getState
561 { journal_file = file_
562 , journal_last_read_time = last_read_time
563 , journal_includes = reverse $ journal_includes journal_
566 -- ** Read 'Journal' from a file
570 ( Consable f ts Transaction
572 , Show (ts Transaction)
574 => Context f ts Transaction
576 -> ExceptT [R.Error Error] IO (Journal (ts Transaction))
580 (liftM Right $ Text.IO.readFile path) $
581 \ko -> return $ Left $
582 [ R.Error_Custom (R.initialPos path) $ Error_reading_file path ko ]
583 >>= liftIO . R.runParserT_with_Error (journal path) ctx path
585 Left ko -> throwE $ ko
586 Right ok -> ExceptT $ return $ Right ok